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/>.
36 from xml
.sax
import saxutils
46 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
49 def __init__(self
, *args
, **kwargs
):
51 def add_signal_receiver(self
, *args
, **kwargs
):
55 def __init__(self
, *args
, **kwargs
):
59 def method(*args
, **kwargs
):
62 def __init__(self
, *args
, **kwargs
):
65 def __init__(self
, *args
, **kwargs
):
69 from gpodder
import feedcore
70 from gpodder
import util
71 from gpodder
import opml
72 from gpodder
import download
73 from gpodder
import my
74 from gpodder
import youtube
75 from gpodder
import player
76 from gpodder
.liblogger
import log
81 from gpodder
.model
import PodcastChannel
82 from gpodder
.model
import PodcastEpisode
83 from gpodder
.dbsqlite
import Database
85 from gpodder
.gtkui
.model
import PodcastListModel
86 from gpodder
.gtkui
.model
import EpisodeListModel
87 from gpodder
.gtkui
.config
import UIConfig
88 from gpodder
.gtkui
.services
import CoverDownloader
89 from gpodder
.gtkui
.widgets
import SimpleMessageArea
90 from gpodder
.gtkui
.desktopfile
import UserAppsReader
92 from gpodder
.gtkui
.draw
import draw_text_box_centered
94 from gpodder
.gtkui
.interface
.common
import BuilderWidget
95 from gpodder
.gtkui
.interface
.common
import TreeViewHelper
96 from gpodder
.gtkui
.interface
.addpodcast
import gPodderAddPodcast
98 if gpodder
.ui
.desktop
:
99 from gpodder
.gtkui
.download
import DownloadStatusModel
101 from gpodder
.gtkui
.desktop
.sync
import gPodderSyncUI
103 from gpodder
.gtkui
.desktop
.channel
import gPodderChannel
104 from gpodder
.gtkui
.desktop
.preferences
import gPodderPreferences
105 from gpodder
.gtkui
.desktop
.shownotes
import gPodderShownotes
106 from gpodder
.gtkui
.desktop
.episodeselector
import gPodderEpisodeSelector
107 from gpodder
.gtkui
.desktop
.podcastdirectory
import gPodderPodcastDirectory
108 from gpodder
.gtkui
.desktop
.dependencymanager
import gPodderDependencyManager
109 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
111 from gpodder
.gtkui
.desktop
.trayicon
import GPodderStatusIcon
113 except Exception, exc
:
114 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
115 log('Warning: This probably means your PyGTK installation is too old!')
116 have_trayicon
= False
117 elif gpodder
.ui
.diablo
:
118 from gpodder
.gtkui
.download
import DownloadStatusModel
120 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
121 from gpodder
.gtkui
.maemo
.preferences
import gPodderPreferences
122 from gpodder
.gtkui
.maemo
.shownotes
import gPodderShownotes
123 from gpodder
.gtkui
.maemo
.episodeselector
import gPodderEpisodeSelector
124 from gpodder
.gtkui
.maemo
.podcastdirectory
import gPodderPodcastDirectory
125 from gpodder
.gtkui
.maemo
.mygpodder
import MygPodderSettings
126 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
127 have_trayicon
= False
128 elif gpodder
.ui
.fremantle
:
129 from gpodder
.gtkui
.frmntl
.model
import DownloadStatusModel
130 from gpodder
.gtkui
.frmntl
.model
import EpisodeListModel
131 from gpodder
.gtkui
.frmntl
.model
import PodcastListModel
133 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
134 from gpodder
.gtkui
.frmntl
.preferences
import gPodderPreferences
135 from gpodder
.gtkui
.frmntl
.shownotes
import gPodderShownotes
136 from gpodder
.gtkui
.frmntl
.episodeselector
import gPodderEpisodeSelector
137 from gpodder
.gtkui
.frmntl
.podcastdirectory
import gPodderPodcastDirectory
138 from gpodder
.gtkui
.frmntl
.episodes
import gPodderEpisodes
139 from gpodder
.gtkui
.frmntl
.downloads
import gPodderDownloads
140 from gpodder
.gtkui
.frmntl
.progress
import ProgressIndicator
141 from gpodder
.gtkui
.frmntl
.widgets
import FancyProgressBar
142 have_trayicon
= False
144 from gpodder
.gtkui
.frmntl
.portrait
import FremantleRotation
145 from gpodder
.gtkui
.frmntl
.mafw
import MafwPlaybackMonitor
146 from gpodder
.gtkui
.frmntl
.hints
import HINT_STRINGS
148 from gpodder
.gtkui
.interface
.common
import Orientation
150 from gpodder
.gtkui
.interface
.welcome
import gPodderWelcome
155 from gpodder
.dbusproxy
import DBusPodcastsProxy
156 from gpodder
import hooks
158 class gPodder(BuilderWidget
, dbus
.service
.Object
):
159 finger_friendly_widgets
= ['btnCleanUpDownloads', 'button_search_episodes_clear']
161 ICON_GENERAL_ADD
= 'general_add'
162 ICON_GENERAL_REFRESH
= 'general_refresh'
164 def __init__(self
, bus_name
, config
):
165 dbus
.service
.Object
.__init
__(self
, object_path
=gpodder
.dbus_gui_object_path
, bus_name
=bus_name
)
166 self
.podcasts_proxy
= DBusPodcastsProxy(lambda: self
.channels
, \
167 self
.on_itemUpdate_activate
, \
168 self
.playback_episodes
, \
169 self
.download_episode_list
, \
170 self
.episode_object_by_uri
, \
172 self
.db
= Database(gpodder
.database_file
)
174 BuilderWidget
.__init
__(self
, None)
177 if gpodder
.ui
.diablo
:
179 self
.app
= hildon
.Program()
180 self
.app
.add_window(self
.main_window
)
181 self
.main_window
.add_toolbar(self
.toolbar
)
183 for child
in self
.main_menu
.get_children():
185 self
.main_window
.set_menu(self
.set_finger_friendly(menu
))
186 self
._last
_orientation
= Orientation
.LANDSCAPE
187 elif gpodder
.ui
.fremantle
:
189 self
.app
= hildon
.Program()
190 self
.app
.add_window(self
.main_window
)
192 appmenu
= hildon
.AppMenu()
194 for filter in (self
.item_view_podcasts_all
, \
195 self
.item_view_podcasts_downloaded
, \
196 self
.item_view_podcasts_unplayed
):
197 button
= gtk
.ToggleButton()
198 filter.connect_proxy(button
)
199 appmenu
.add_filter(button
)
201 for action
in (self
.itemPreferences
, \
202 self
.item_downloads
, \
203 self
.itemRemoveOldEpisodes
, \
204 self
.item_unsubscribe
, \
206 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
207 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
208 action
.connect_proxy(button
)
209 if action
== self
.item_downloads
:
210 button
.set_title(_('Downloads'))
211 button
.set_value(_('Idle'))
212 self
.button_downloads
= button
213 appmenu
.append(button
)
215 def show_hint(button
):
216 self
.show_message(random
.choice(HINT_STRINGS
), important
=True)
218 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
219 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
220 button
.set_title(_('Hint of the day'))
221 button
.connect('clicked', show_hint
)
222 appmenu
.append(button
)
225 self
.main_window
.set_app_menu(appmenu
)
227 # Initialize portrait mode / rotation manager
228 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
230 gpodder
.__version
__, \
231 self
.config
.rotation_mode
)
233 if self
.config
.rotation_mode
== FremantleRotation
.ALWAYS
:
234 util
.idle_add(self
.on_window_orientation_changed
, \
235 Orientation
.PORTRAIT
)
236 self
._last
_orientation
= Orientation
.PORTRAIT
238 self
._last
_orientation
= Orientation
.LANDSCAPE
240 # Flag set when a notification is being shown (Maemo bug 11235)
241 self
._fremantle
_notification
_visible
= False
243 self
._last
_orientation
= Orientation
.LANDSCAPE
244 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
246 self
.bluetooth_available
= util
.bluetooth_available()
248 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
249 if not gpodder
.ui
.fremantle
:
250 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
251 self
.main_window
.show()
253 self
.player_receiver
= player
.MediaPlayerDBusReceiver(self
.on_played
)
255 if gpodder
.ui
.fremantle
:
256 # Create a D-Bus monitoring object that takes care of
257 # tracking MAFW (Nokia Media Player) playback events
258 # and sends episode playback status events via D-Bus
259 self
.mafw_monitor
= MafwPlaybackMonitor(gpodder
.dbus_session_bus
)
261 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
263 self
.preferences_dialog
= None
264 self
.config
.add_observer(self
.on_config_changed
)
266 self
.tray_icon
= None
267 self
.episode_shownotes_window
= None
268 self
.new_episodes_window
= None
270 if gpodder
.ui
.desktop
:
271 # Mac OS X-specific UI tweaks: Native main menu integration
272 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
273 if getattr(gtk
.gdk
, 'WINDOWING', 'x11') == 'quartz':
275 import igemacintegration
as igemi
277 # Move the menu bar from the window to the Mac menu bar
279 igemi
.ige_mac_menu_set_menu_bar(self
.mainMenu
)
281 # Reparent some items to the "Application" menu
282 for widget
in ('/mainMenu/menuHelp/itemAbout', \
283 '/mainMenu/menuPodcasts/itemPreferences'):
284 item
= self
.uimanager1
.get_widget(widget
)
285 group
= igemi
.ige_mac_menu_add_app_menu_group()
286 igemi
.ige_mac_menu_add_app_menu_item(group
, item
, None)
288 quit_widget
= '/mainMenu/menuPodcasts/itemQuit'
289 quit_item
= self
.uimanager1
.get_widget(quit_widget
)
290 igemi
.ige_mac_menu_set_quit_menu_item(quit_item
)
292 print >>sys
.stderr
, """
293 Warning: ige-mac-integration not found - no native menus.
296 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
297 self
.main_window
, self
.show_confirmation
, \
298 self
.update_episode_list_icons
, \
299 self
.update_podcast_list_model
, self
.toolPreferences
, \
300 gPodderEpisodeSelector
, \
301 self
.commit_changes_to_database
)
305 self
.download_status_model
= DownloadStatusModel()
306 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
308 if gpodder
.ui
.desktop
:
309 self
.show_hide_tray_icon()
310 self
.itemShowAllEpisodes
.set_active(self
.config
.podcast_list_view_all
)
311 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
312 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
314 if not gpodder
.ui
.fremantle
:
315 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
316 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
317 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
318 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
320 # When the amount of maximum downloads changes, notify the queue manager
321 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_threads()
322 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
324 self
.default_title
= 'gPodder'
325 if gpodder
.__version
__.rfind('git') != -1:
326 self
.set_title('gPodder %s' % gpodder
.__version
__)
328 title
= self
.gPodder
.get_title()
329 if title
is not None:
330 self
.set_title(title
)
332 self
.set_title(_('gPodder'))
334 self
.cover_downloader
= CoverDownloader()
336 # Generate list models for podcasts and their episodes
337 self
.podcast_list_model
= PodcastListModel(self
.cover_downloader
)
339 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
340 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
342 if gpodder
.ui
.fremantle
:
343 # Work around Maemo bug #4718
344 self
.button_refresh
.set_name('HildonButton-finger')
345 self
.button_subscribe
.set_name('HildonButton-finger')
347 self
.button_refresh
.set_sensitive(False)
348 self
.button_subscribe
.set_sensitive(False)
350 self
.button_subscribe
.set_image(gtk
.image_new_from_icon_name(\
351 self
.ICON_GENERAL_ADD
, gtk
.ICON_SIZE_BUTTON
))
352 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
353 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
355 # Make the button scroll together with the TreeView contents
356 action_area_box
= self
.treeChannels
.get_action_area_box()
357 for child
in self
.buttonbox
:
358 child
.reparent(action_area_box
)
359 self
.vbox
.remove(self
.buttonbox
)
360 action_area_box
.set_spacing(2)
361 action_area_box
.set_border_width(3)
362 self
.treeChannels
.set_action_area_visible(True)
364 # Set up a very nice progress bar setup
365 self
.fancy_progress_bar
= FancyProgressBar(self
.main_window
, \
366 self
.on_btnCancelFeedUpdate_clicked
)
367 self
.pbFeedUpdate
= self
.fancy_progress_bar
.progress_bar
368 self
.pbFeedUpdate
.set_ellipsize(pango
.ELLIPSIZE_MIDDLE
)
369 self
.vbox
.pack_start(self
.fancy_progress_bar
.event_box
, False)
371 from gpodder
.gtkui
.frmntl
import style
372 sub_font
= style
.get_font_desc('SmallSystemFont')
373 sub_color
= style
.get_color('SecondaryTextColor')
374 sub
= (sub_font
.to_string(), sub_color
.to_string())
375 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
376 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
378 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
379 while gtk
.events_pending():
380 gtk
.main_iteration(False)
383 # Try to get the real package version from dpkg
384 p
= subprocess
.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout
=subprocess
.PIPE
)
385 version
, _stderr
= p
.communicate()
389 version
= gpodder
.__version
__
390 self
.label_footer
.set_markup(sub
% ('v %s' % version
))
391 self
.label_footer
.hide()
393 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
394 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
395 show_episode_shownotes
=self
.show_episode_shownotes
, \
396 update_podcast_list_model
=self
.update_podcast_list_model
, \
397 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
398 item_view_episodes_all
=self
.item_view_episodes_all
, \
399 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
400 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
401 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
402 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
403 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
404 hide_episode_search
=self
.hide_episode_search
, \
405 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
406 playback_episodes
=self
.playback_episodes
, \
407 delete_episode_list
=self
.delete_episode_list
, \
408 episode_list_status_changed
=self
.episode_list_status_changed
, \
409 download_episode_list
=self
.download_episode_list
, \
410 episode_is_downloading
=self
.episode_is_downloading
, \
411 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
412 add_download_task_monitor
=self
.add_download_task_monitor
, \
413 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
414 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
, \
415 on_delete_episodes_button_clicked
=self
.on_itemRemoveOldEpisodes_activate
, \
416 on_itemUpdate_activate
=self
.on_itemUpdate_activate
)
418 # Expose objects for episode list type-ahead find
419 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
420 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
421 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
423 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
424 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
425 cleanup_downloads
=self
.cleanup_downloads
, \
426 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
427 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
430 self
.treeAvailable
= self
.episodes_window
.treeview
431 self
.treeDownloads
= self
.downloads_window
.treeview
433 # Init the treeviews that we use
434 self
.init_podcast_list_treeview()
435 self
.init_episode_list_treeview()
436 self
.init_download_list_treeview()
438 if self
.config
.podcast_list_hide_boring
:
439 self
.item_view_hide_boring_podcasts
.set_active(True)
441 self
.currently_updating
= False
444 self
.context_menu_mouse_button
= 1
446 self
.context_menu_mouse_button
= 3
448 if self
.config
.start_iconified
:
449 self
.iconify_main_window()
451 self
.download_tasks_seen
= set()
452 self
.download_list_update_enabled
= False
453 self
.download_task_monitors
= set()
455 # Subscribed channels
456 self
.active_channel
= None
457 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
458 self
.channel_list_changed
= True
459 self
.update_podcasts_tab()
461 # load list of user applications for audio playback
462 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
463 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
465 # Set the "Device" menu item for the first time
466 if gpodder
.ui
.desktop
:
467 self
.update_item_device()
469 # Set up the first instance of MygPoClient
470 self
.mygpo_client
= my
.MygPoClient(self
.config
)
472 # Now, update the feed cache, when everything's in place
473 if not gpodder
.ui
.fremantle
:
474 self
.btnUpdateFeeds
.show()
475 self
.updating_feed_cache
= False
476 self
.feed_cache_update_cancelled
= False
477 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
479 self
.message_area
= None
481 def find_partial_downloads():
482 # Look for partial file downloads
483 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
484 count
= len(partial_files
)
485 resumable_episodes
= []
487 if not gpodder
.ui
.fremantle
:
488 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
489 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
490 _('Some episodes have not finished downloading in a previous session.'), \
491 False, self
.get_dialog_parent())
492 indicator
.on_message(N_('%d partial file', '%d partial files', count
) % count
)
494 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
497 for c
in self
.channels
:
498 for e
in c
.get_all_episodes():
499 filename
= e
.local_filename(create
=False, check_only
=True)
500 if filename
in candidates
:
501 log('Found episode: %s', e
.title
, sender
=self
)
503 indicator
.on_message(e
.title
)
504 indicator
.on_progress(float(found
)/count
)
505 candidates
.remove(filename
)
506 partial_files
.remove(filename
+'.partial')
507 resumable_episodes
.append(e
)
515 for f
in partial_files
:
516 log('Partial file without episode: %s', f
, sender
=self
)
519 util
.idle_add(indicator
.on_finished
)
521 if len(resumable_episodes
):
522 def offer_resuming():
523 self
.download_episode_list_paused(resumable_episodes
)
524 if not gpodder
.ui
.fremantle
:
525 resume_all
= gtk
.Button(_('Resume all'))
526 #resume_all.set_border_width(0)
527 def on_resume_all(button
):
528 selection
= self
.treeDownloads
.get_selection()
529 selection
.select_all()
530 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
531 selection
.unselect_all()
532 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
533 self
.message_area
.hide()
534 resume_all
.connect('clicked', on_resume_all
)
536 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
537 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
538 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
539 self
.message_area
.show_all()
540 self
.clean_up_downloads(delete_partial
=False)
541 util
.idle_add(offer_resuming
)
542 elif not gpodder
.ui
.fremantle
:
543 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
545 util
.idle_add(self
.clean_up_downloads
, True)
546 threading
.Thread(target
=find_partial_downloads
).start()
548 # Start the auto-update procedure
549 self
._auto
_update
_timer
_source
_id
= None
550 if self
.config
.auto_update_feeds
:
551 self
.restart_auto_update_timer()
553 # Delete old episodes if the user wishes to
554 if self
.config
.auto_remove_played_episodes
and \
555 self
.config
.episode_old_age
> 0:
556 old_episodes
= list(self
.get_expired_episodes())
557 if len(old_episodes
) > 0:
558 self
.delete_episode_list(old_episodes
, confirm
=False)
559 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
561 if gpodder
.ui
.fremantle
:
562 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
563 self
.button_refresh
.set_sensitive(True)
564 self
.button_subscribe
.set_sensitive(True)
565 self
.main_window
.set_title(_('gPodder'))
566 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
568 # Do the initial sync with the web service
569 util
.idle_add(self
.mygpo_client
.flush
, True)
571 # First-time users should be asked if they want to see the OPML
572 if not self
.channels
and not gpodder
.ui
.fremantle
:
573 util
.idle_add(self
.on_itemUpdate_activate
)
575 def episode_object_by_uri(self
, uri
):
576 """Get an episode object given a local or remote URI
578 This can be used to quickly access an episode object
579 when all we have is its download filename or episode
580 URL (e.g. from external D-Bus calls / signals, etc..)
582 if uri
.startswith('/'):
583 uri
= 'file://' + uri
585 prefix
= 'file://' + self
.config
.download_dir
587 if uri
.startswith(prefix
):
588 # File is on the local filesystem in the download folder
589 filename
= uri
[len(prefix
):]
590 file_parts
= [x
for x
in filename
.split(os
.sep
) if x
]
592 if len(file_parts
) == 2:
593 dir_name
, filename
= file_parts
594 channels
= [c
for c
in self
.channels
if c
.foldername
== dir_name
]
595 if len(channels
) == 1:
596 channel
= channels
[0]
597 return channel
.get_episode_by_filename(filename
)
599 # Possibly remote file - search the database for a podcast
600 channel_id
= self
.db
.get_channel_id_from_episode_url(uri
)
602 if channel_id
is not None:
603 channels
= [c
for c
in self
.channels
if c
.id == channel_id
]
604 if len(channels
) == 1:
605 channel
= channels
[0]
606 return channel
.get_episode_by_url(uri
)
610 def on_played(self
, start
, end
, total
, file_uri
):
611 """Handle the "played" signal from a media player"""
612 if start
== 0 and end
== 0 and total
== 0:
613 # Ignore bogus play event
615 elif end
< start
+ 5:
616 # Ignore "less than five seconds" segments,
617 # as they can happen with seeking, etc...
620 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
621 episode
= self
.episode_object_by_uri(file_uri
)
623 if episode
is not None:
624 file_type
= episode
.file_type()
625 # Automatically enable D-Bus played status mode
626 if file_type
== 'audio':
627 self
.config
.audio_played_dbus
= True
628 elif file_type
== 'video':
629 self
.config
.video_played_dbus
= True
633 episode
.total_time
= total
635 # Assume the episode's total time for the action
636 total
= episode
.total_time
637 if episode
.current_position_updated
is None or \
638 now
> episode
.current_position_updated
:
639 episode
.current_position
= end
640 episode
.current_position_updated
= now
641 episode
.mark(is_played
=True)
644 self
.update_episode_list_icons([episode
.url
])
645 self
.update_podcast_list_model([episode
.channel
.url
])
647 # Submit this action to the webservice
648 self
.mygpo_client
.on_playback_full(episode
, \
651 def on_add_remove_podcasts_mygpo(self
):
652 actions
= self
.mygpo_client
.get_received_actions()
656 existing_urls
= [c
.url
for c
in self
.channels
]
658 # Columns for the episode selector window - just one...
660 ('description', None, None, _('Action')),
663 # A list of actions that have to be chosen from
666 # Actions that are ignored (already carried out)
669 for action
in actions
:
670 if action
.is_add
and action
.url
not in existing_urls
:
671 changes
.append(my
.Change(action
))
672 elif action
.is_remove
and action
.url
in existing_urls
:
673 podcast_object
= None
674 for podcast
in self
.channels
:
675 if podcast
.url
== action
.url
:
676 podcast_object
= podcast
678 changes
.append(my
.Change(action
, podcast_object
))
680 log('Ignoring action: %s', action
, sender
=self
)
681 ignored
.append(action
)
683 # Confirm all ignored changes
684 self
.mygpo_client
.confirm_received_actions(ignored
)
686 def execute_podcast_actions(selected
):
687 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
688 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
690 # Apply the accepted changes locally
691 self
.add_podcast_list(add_list
)
692 self
.remove_podcast_list(remove_list
, confirm
=False)
694 # All selected items are now confirmed
695 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
697 # Revert the changes on the server
698 rejected
= [c
.action
for c
in changes
if c
not in selected
]
699 self
.mygpo_client
.reject_received_actions(rejected
)
702 # We're abusing the Episode Selector again ;) -- thp
703 gPodderEpisodeSelector(self
.main_window
, \
704 title
=_('Confirm changes from gpodder.net'), \
705 instructions
=_('Select the actions you want to carry out.'), \
708 size_attribute
=None, \
709 stock_ok_button
=gtk
.STOCK_APPLY
, \
710 callback
=execute_podcast_actions
, \
713 # There are some actions that need the user's attention
718 # We have no remaining actions - no selection happens
721 def rewrite_urls_mygpo(self
):
722 # Check if we have to rewrite URLs since the last add
723 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
725 for rewritten_url
in rewritten_urls
:
726 if not rewritten_url
.new_url
:
729 for channel
in self
.channels
:
730 if channel
.url
== rewritten_url
.old_url
:
731 log('Updating URL of %s to %s', channel
, \
732 rewritten_url
.new_url
, sender
=self
)
733 channel
.url
= rewritten_url
.new_url
735 self
.channel_list_changed
= True
736 util
.idle_add(self
.update_episode_list_model
)
739 def on_send_full_subscriptions(self
):
740 # Send the full subscription list to the gpodder.net client
741 # (this will overwrite the subscription list on the server)
742 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
743 _('Your subscriptions are being uploaded to the server.'), \
744 False, self
.get_dialog_parent())
747 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
748 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
753 message
= e
.__class
__.__name
__
754 self
.show_message(message
, \
755 _('Error while uploading'), \
757 util
.idle_add(show_error
, e
)
759 util
.idle_add(indicator
.on_finished
)
761 def on_podcast_selected(self
, treeview
, path
, column
):
763 model
= treeview
.get_model()
764 channel
= model
.get_value(model
.get_iter(path
), \
765 PodcastListModel
.C_CHANNEL
)
766 self
.active_channel
= channel
767 self
.update_episode_list_model()
768 self
.episodes_window
.channel
= self
.active_channel
769 self
.episodes_window
.show()
771 def on_button_subscribe_clicked(self
, button
):
772 self
.on_itemImportChannels_activate(button
)
774 def on_button_downloads_clicked(self
, widget
):
775 self
.downloads_window
.show()
777 def show_episode_in_download_manager(self
, episode
):
778 self
.downloads_window
.show()
779 model
= self
.treeDownloads
.get_model()
780 selection
= self
.treeDownloads
.get_selection()
781 selection
.unselect_all()
782 it
= model
.get_iter_first()
783 while it
is not None:
784 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
785 if task
.episode
.url
== episode
.url
:
786 selection
.select_iter(it
)
787 # FIXME: Scroll to selection in pannable area
789 it
= model
.iter_next(it
)
791 def for_each_episode_set_task_status(self
, episodes
, status
):
792 episode_urls
= set(episode
.url
for episode
in episodes
)
793 model
= self
.treeDownloads
.get_model()
794 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
795 model
.get_value(row
.iter, \
796 DownloadStatusModel
.C_TASK
)) for row
in model \
797 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
799 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
801 def on_window_orientation_changed(self
, orientation
):
802 self
._last
_orientation
= orientation
803 if self
.preferences_dialog
is not None:
804 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
806 treeview
= self
.treeChannels
807 if orientation
== Orientation
.PORTRAIT
:
808 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
809 # Work around Maemo bug #4718
810 self
.button_subscribe
.set_name('HildonButton-thumb')
811 self
.button_refresh
.set_name('HildonButton-thumb')
813 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
814 # Work around Maemo bug #4718
815 self
.button_subscribe
.set_name('HildonButton-finger')
816 self
.button_refresh
.set_name('HildonButton-finger')
818 if gpodder
.ui
.fremantle
:
819 self
.fancy_progress_bar
.relayout()
821 def on_treeview_podcasts_selection_changed(self
, selection
):
822 model
, iter = selection
.get_selected()
824 self
.active_channel
= None
825 self
.episode_list_model
.clear()
827 def on_treeview_button_pressed(self
, treeview
, event
):
828 if event
.window
!= treeview
.get_bin_window():
831 TreeViewHelper
.save_button_press_event(treeview
, event
)
833 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
834 TreeViewHelper
.ROLE_PODCASTS
:
835 return self
.currently_updating
837 return event
.button
== self
.context_menu_mouse_button
and \
840 def on_treeview_podcasts_button_released(self
, treeview
, event
):
841 if event
.window
!= treeview
.get_bin_window():
845 return self
.treeview_channels_handle_gestures(treeview
, event
)
846 return self
.treeview_channels_show_context_menu(treeview
, event
)
848 def on_treeview_episodes_button_released(self
, treeview
, event
):
849 if event
.window
!= treeview
.get_bin_window():
853 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
854 return self
.treeview_available_handle_gestures(treeview
, event
)
856 return self
.treeview_available_show_context_menu(treeview
, event
)
858 def on_treeview_downloads_button_released(self
, treeview
, event
):
859 if event
.window
!= treeview
.get_bin_window():
862 return self
.treeview_downloads_show_context_menu(treeview
, event
)
864 def on_entry_search_podcasts_changed(self
, editable
):
865 if self
.hbox_search_podcasts
.get_property('visible'):
866 self
.podcast_list_model
.set_search_term(editable
.get_chars(0, -1))
868 def on_entry_search_podcasts_key_press(self
, editable
, event
):
869 if event
.keyval
== gtk
.keysyms
.Escape
:
870 self
.hide_podcast_search()
873 def hide_podcast_search(self
, *args
):
874 self
.hbox_search_podcasts
.hide()
875 self
.entry_search_podcasts
.set_text('')
876 self
.podcast_list_model
.set_search_term(None)
877 self
.treeChannels
.grab_focus()
879 def show_podcast_search(self
, input_char
):
880 self
.hbox_search_podcasts
.show()
881 self
.entry_search_podcasts
.insert_text(input_char
, -1)
882 self
.entry_search_podcasts
.grab_focus()
883 self
.entry_search_podcasts
.set_position(-1)
885 def init_podcast_list_treeview(self
):
886 # Set up podcast channel tree view widget
887 if gpodder
.ui
.fremantle
:
888 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
889 self
.item_view_podcasts_downloaded
.set_active(True)
890 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
891 self
.item_view_podcasts_unplayed
.set_active(True)
893 self
.item_view_podcasts_all
.set_active(True)
894 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
896 iconcolumn
= gtk
.TreeViewColumn('')
897 iconcell
= gtk
.CellRendererPixbuf()
898 iconcolumn
.pack_start(iconcell
, False)
899 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
900 self
.treeChannels
.append_column(iconcolumn
)
902 namecolumn
= gtk
.TreeViewColumn('')
903 namecell
= gtk
.CellRendererText()
904 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
905 namecolumn
.pack_start(namecell
, True)
906 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
908 if gpodder
.ui
.fremantle
:
909 countcell
= gtk
.CellRendererText()
910 from gpodder
.gtkui
.frmntl
import style
911 countcell
.set_property('font-desc', style
.get_font_desc('EmpSystemFont'))
912 countcell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
913 countcell
.set_property('alignment', pango
.ALIGN_RIGHT
)
914 countcell
.set_property('xalign', 1.)
915 countcell
.set_property('xpad', 5)
916 namecolumn
.pack_start(countcell
, False)
917 namecolumn
.add_attribute(countcell
, 'text', PodcastListModel
.C_DOWNLOADS
)
918 namecolumn
.add_attribute(countcell
, 'visible', PodcastListModel
.C_DOWNLOADS
)
920 iconcell
= gtk
.CellRendererPixbuf()
921 iconcell
.set_property('xalign', 1.0)
922 namecolumn
.pack_start(iconcell
, False)
923 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
924 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
926 self
.treeChannels
.append_column(namecolumn
)
928 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
930 # When no podcast is selected, clear the episode list model
931 selection
= self
.treeChannels
.get_selection()
932 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
934 # Set up type-ahead find for the podcast list
935 def on_key_press(treeview
, event
):
936 if event
.keyval
== gtk
.keysyms
.Escape
:
937 self
.hide_podcast_search()
938 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
939 self
.hide_podcast_search()
940 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
941 # Don't handle type-ahead when control is pressed (so shortcuts
942 # with the Ctrl key still work, e.g. Ctrl+A, ...)
945 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
946 if unicode_char_id
== 0:
948 input_char
= unichr(unicode_char_id
)
949 self
.show_podcast_search(input_char
)
951 self
.treeChannels
.connect('key-press-event', on_key_press
)
953 # Enable separators to the podcast list to separate special podcasts
954 # from others (this is used for the "all episodes" view)
955 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
957 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
959 def on_entry_search_episodes_changed(self
, editable
):
960 if self
.hbox_search_episodes
.get_property('visible'):
961 self
.episode_list_model
.set_search_term(editable
.get_chars(0, -1))
963 def on_entry_search_episodes_key_press(self
, editable
, event
):
964 if event
.keyval
== gtk
.keysyms
.Escape
:
965 self
.hide_episode_search()
968 def hide_episode_search(self
, *args
):
969 self
.hbox_search_episodes
.hide()
970 self
.entry_search_episodes
.set_text('')
971 self
.episode_list_model
.set_search_term(None)
972 self
.treeAvailable
.grab_focus()
974 def show_episode_search(self
, input_char
):
975 self
.hbox_search_episodes
.show()
976 self
.entry_search_episodes
.insert_text(input_char
, -1)
977 self
.entry_search_episodes
.grab_focus()
978 self
.entry_search_episodes
.set_position(-1)
980 def init_episode_list_treeview(self
):
981 # For loading the list model
982 self
.empty_episode_list_model
= EpisodeListModel()
983 self
.episode_list_model
= EpisodeListModel()
985 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
986 self
.item_view_episodes_undeleted
.set_active(True)
987 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
988 self
.item_view_episodes_downloaded
.set_active(True)
989 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
990 self
.item_view_episodes_unplayed
.set_active(True)
992 self
.item_view_episodes_all
.set_active(True)
994 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
996 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
998 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
1000 iconcell
= gtk
.CellRendererPixbuf()
1001 if gpodder
.ui
.maemo
:
1002 iconcell
.set_fixed_size(50, 50)
1003 status_column_label
= ''
1005 status_column_label
= _('Status')
1006 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=EpisodeListModel
.C_STATUS_ICON
)
1008 namecell
= gtk
.CellRendererText()
1009 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1010 namecolumn
= gtk
.TreeViewColumn(_('Episode'))
1011 namecolumn
.pack_start(namecell
, True)
1012 namecolumn
.add_attribute(namecell
, 'markup', EpisodeListModel
.C_DESCRIPTION
)
1013 namecolumn
.set_sort_column_id(EpisodeListModel
.C_DESCRIPTION
)
1014 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1015 namecolumn
.set_resizable(True)
1016 namecolumn
.set_expand(True)
1018 if gpodder
.ui
.fremantle
:
1019 from gpodder
.gtkui
.frmntl
import style
1020 timecell
= gtk
.CellRendererText()
1021 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1022 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1023 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1024 timecell
.set_property('xalign', 1.)
1025 timecell
.set_property('xpad', 5)
1026 namecolumn
.pack_start(timecell
, False)
1027 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1028 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME1_VISIBLE
)
1030 # Add another cell renderer to fix a sizing issue (one renderer
1031 # only renders short text and the other one longer text to avoid
1032 # having titles of episodes unnecessarily cut off)
1033 timecell
= gtk
.CellRendererText()
1034 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1035 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1036 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1037 timecell
.set_property('xalign', 1.)
1038 timecell
.set_property('xpad', 5)
1039 namecolumn
.pack_start(timecell
, False)
1040 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1041 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME2_VISIBLE
)
1043 sizecell
= gtk
.CellRendererText()
1044 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
1045 sizecolumn
.set_sort_column_id(EpisodeListModel
.C_FILESIZE
)
1047 releasecell
= gtk
.CellRendererText()
1048 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
1049 releasecolumn
.set_sort_column_id(EpisodeListModel
.C_PUBLISHED
)
1051 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
1052 itemcolumn
.set_reorderable(True)
1053 self
.treeAvailable
.append_column(itemcolumn
)
1055 if gpodder
.ui
.maemo
:
1056 sizecolumn
.set_visible(False)
1057 releasecolumn
.set_visible(False)
1059 # Set up type-ahead find for the episode list
1060 def on_key_press(treeview
, event
):
1061 if event
.keyval
== gtk
.keysyms
.Escape
:
1062 self
.hide_episode_search()
1063 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
1064 self
.hide_episode_search()
1065 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
1066 # Don't handle type-ahead when control is pressed (so shortcuts
1067 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1070 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1071 if unicode_char_id
== 0:
1073 input_char
= unichr(unicode_char_id
)
1074 self
.show_episode_search(input_char
)
1076 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1078 if gpodder
.ui
.desktop
:
1079 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1080 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1081 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1082 if self
.config
.on_drag_mark_played
:
1083 for episode
in self
.get_selected_episodes():
1084 episode
.mark(is_played
=True)
1085 self
.on_selected_episodes_status_changed()
1086 uris
= ['file://'+e
.local_filename(create
=False) \
1087 for e
in self
.get_selected_episodes() \
1088 if e
.was_downloaded(and_exists
=True)]
1089 uris
.append('') # for the trailing '\r\n'
1090 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1091 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1093 selection
= self
.treeAvailable
.get_selection()
1094 if gpodder
.ui
.diablo
:
1095 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
1096 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1098 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1099 elif gpodder
.ui
.fremantle
:
1100 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1102 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1103 # Update the sensitivity of the toolbar buttons on the Desktop
1104 selection
.connect('changed', lambda s
: self
.play_or_download())
1106 if gpodder
.ui
.diablo
:
1107 # Set up the tap-and-hold context menu for podcasts
1109 menu
.append(self
.itemUpdateChannel
.create_menu_item())
1110 menu
.append(self
.itemEditChannel
.create_menu_item())
1111 menu
.append(gtk
.SeparatorMenuItem())
1112 menu
.append(self
.itemRemoveChannel
.create_menu_item())
1113 menu
.append(gtk
.SeparatorMenuItem())
1114 item
= gtk
.ImageMenuItem(_('Close this menu'))
1115 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1116 gtk
.ICON_SIZE_MENU
))
1119 menu
= self
.set_finger_friendly(menu
)
1120 self
.treeChannels
.tap_and_hold_setup(menu
)
1123 def init_download_list_treeview(self
):
1124 # enable multiple selection support
1125 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1126 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1128 # columns and renderers for "download progress" tab
1129 # First column: [ICON] Episodename
1130 column
= gtk
.TreeViewColumn(_('Episode'))
1132 cell
= gtk
.CellRendererPixbuf()
1133 if gpodder
.ui
.maemo
:
1134 cell
.set_fixed_size(50, 50)
1135 cell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1136 column
.pack_start(cell
, expand
=False)
1137 column
.add_attribute(cell
, 'stock-id', \
1138 DownloadStatusModel
.C_ICON_NAME
)
1140 cell
= gtk
.CellRendererText()
1141 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1142 column
.pack_start(cell
, expand
=True)
1143 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1144 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1145 column
.set_expand(True)
1146 self
.treeDownloads
.append_column(column
)
1148 # Second column: Progress
1149 cell
= gtk
.CellRendererProgress()
1150 cell
.set_property('yalign', .5)
1151 cell
.set_property('ypad', 6)
1152 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1153 value
=DownloadStatusModel
.C_PROGRESS
, \
1154 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1155 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1156 column
.set_expand(False)
1157 self
.treeDownloads
.append_column(column
)
1158 column
.set_property('min-width', 150)
1159 column
.set_property('max-width', 150)
1161 self
.treeDownloads
.set_model(self
.download_status_model
)
1162 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1164 def on_treeview_expose_event(self
, treeview
, event
):
1165 if event
.window
== treeview
.get_bin_window():
1166 model
= treeview
.get_model()
1167 if (model
is not None and model
.get_iter_first() is not None):
1170 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1174 ctx
= event
.window
.cairo_create()
1175 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1176 event
.area
.width
, event
.area
.height
)
1179 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1182 if role
== TreeViewHelper
.ROLE_EPISODES
:
1183 if self
.currently_updating
:
1184 text
= _('Loading episodes')
1185 progress
= self
.episode_list_model
.get_update_progress()
1186 elif self
.config
.episode_list_view_mode
!= \
1187 EpisodeListModel
.VIEW_ALL
:
1188 text
= _('No episodes in current view')
1190 text
= _('No episodes available')
1191 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1192 if self
.config
.episode_list_view_mode
!= \
1193 EpisodeListModel
.VIEW_ALL
and \
1194 self
.config
.podcast_list_hide_boring
and \
1195 len(self
.channels
) > 0:
1196 text
= _('No podcasts in this view')
1198 text
= _('No subscriptions')
1199 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1200 text
= _('No active downloads')
1202 raise Exception('on_treeview_expose_event: unknown role')
1204 if gpodder
.ui
.fremantle
:
1205 from gpodder
.gtkui
.frmntl
import style
1206 font_desc
= style
.get_font_desc('LargeSystemFont')
1210 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1214 def enable_download_list_update(self
):
1215 if not self
.download_list_update_enabled
:
1216 self
.update_downloads_list()
1217 gobject
.timeout_add(1500, self
.update_downloads_list
)
1218 self
.download_list_update_enabled
= True
1220 def cleanup_downloads(self
):
1221 model
= self
.download_status_model
1223 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1224 changed_episode_urls
= set()
1225 for row_reference
, task
in all_tasks
:
1226 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1227 model
.remove(model
.get_iter(row_reference
.get_path()))
1229 # We don't "see" this task anymore - remove it;
1230 # this is needed, so update_episode_list_icons()
1231 # below gets the correct list of "seen" tasks
1232 self
.download_tasks_seen
.remove(task
)
1233 except KeyError, key_error
:
1234 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1235 changed_episode_urls
.add(task
.url
)
1236 # Tell the task that it has been removed (so it can clean up)
1237 task
.removed_from_list()
1239 # Tell the podcasts tab to update icons for our removed podcasts
1240 self
.update_episode_list_icons(changed_episode_urls
)
1242 # Tell the shownotes window that we have removed the episode
1243 if self
.episode_shownotes_window
is not None and \
1244 self
.episode_shownotes_window
.episode
is not None and \
1245 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1246 self
.episode_shownotes_window
._download
_status
_changed
(None)
1248 # Update the downloads list one more time
1249 self
.update_downloads_list(can_call_cleanup
=False)
1251 def on_tool_downloads_toggled(self
, toolbutton
):
1252 if toolbutton
.get_active():
1253 self
.wNotebook
.set_current_page(1)
1255 self
.wNotebook
.set_current_page(0)
1257 def add_download_task_monitor(self
, monitor
):
1258 self
.download_task_monitors
.add(monitor
)
1259 model
= self
.download_status_model
1263 task
= row
[self
.download_status_model
.C_TASK
]
1264 monitor
.task_updated(task
)
1266 def remove_download_task_monitor(self
, monitor
):
1267 self
.download_task_monitors
.remove(monitor
)
1269 def update_downloads_list(self
, can_call_cleanup
=True):
1271 model
= self
.download_status_model
1273 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1274 total_speed
, total_size
, done_size
= 0, 0, 0
1276 # Keep a list of all download tasks that we've seen
1277 download_tasks_seen
= set()
1279 # Remember the DownloadTask object for the episode that
1280 # has been opened in the episode shownotes dialog (if any)
1281 if self
.episode_shownotes_window
is not None:
1282 shownotes_episode
= self
.episode_shownotes_window
.episode
1283 shownotes_task
= None
1285 shownotes_episode
= None
1286 shownotes_task
= None
1288 # Do not go through the list of the model is not (yet) available
1292 failed_downloads
= []
1294 self
.download_status_model
.request_update(row
.iter)
1296 task
= row
[self
.download_status_model
.C_TASK
]
1297 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1299 # Let the download task monitors know of changes
1300 for monitor
in self
.download_task_monitors
:
1301 monitor
.task_updated(task
)
1304 done_size
+= size
*progress
1306 if shownotes_episode
is not None and \
1307 shownotes_episode
.url
== task
.episode
.url
:
1308 shownotes_task
= task
1310 download_tasks_seen
.add(task
)
1312 if status
== download
.DownloadTask
.DOWNLOADING
:
1314 total_speed
+= speed
1315 elif status
== download
.DownloadTask
.FAILED
:
1316 failed_downloads
.append(task
)
1318 elif status
== download
.DownloadTask
.DONE
:
1320 elif status
== download
.DownloadTask
.QUEUED
:
1322 elif status
== download
.DownloadTask
.PAUSED
:
1327 # Remember which tasks we have seen after this run
1328 self
.download_tasks_seen
= download_tasks_seen
1330 if gpodder
.ui
.desktop
:
1331 text
= [_('Downloads')]
1332 if downloading
+ failed
+ queued
> 0:
1335 s
.append(N_('%d active', '%d active', downloading
) % downloading
)
1337 s
.append(N_('%d failed', '%d failed', failed
) % failed
)
1339 s
.append(N_('%d queued', '%d queued', queued
) % queued
)
1340 text
.append(' (' + ', '.join(s
)+')')
1341 self
.labelDownloads
.set_text(''.join(text
))
1342 elif gpodder
.ui
.diablo
:
1343 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1345 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1347 self
.tool_downloads
.set_label(_('Downloads'))
1348 elif gpodder
.ui
.fremantle
:
1349 if downloading
+ queued
> 0:
1350 self
.button_downloads
.set_value(N_('%d active', '%d active', downloading
+queued
) % (downloading
+queued
))
1352 self
.button_downloads
.set_value(N_('%d failed', '%d failed', failed
) % failed
)
1354 self
.button_downloads
.set_value(N_('%d paused', '%d paused', paused
) % paused
)
1356 self
.button_downloads
.set_value(_('Idle'))
1358 title
= [self
.default_title
]
1360 # We have to update all episodes/channels for which the status has
1361 # changed. Accessing task.status_changed has the side effect of
1362 # re-setting the changed flag, so we need to get the "changed" list
1363 # of tuples first and split it into two lists afterwards
1364 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1365 self
.download_tasks_seen
if task
.status_changed
]
1366 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1367 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1369 count
= downloading
+ queued
1371 title
.append(N_('downloading %d file', 'downloading %d files', count
) % count
)
1374 percentage
= 100.0*done_size
/total_size
1377 total_speed
= util
.format_filesize(total_speed
)
1378 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1379 if self
.tray_icon
is not None:
1380 # Update the tray icon status and progress bar
1381 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1382 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1384 if self
.tray_icon
is not None:
1385 # Update the tray icon status
1386 self
.tray_icon
.set_status()
1387 if gpodder
.ui
.desktop
:
1388 self
.downloads_finished(self
.download_tasks_seen
)
1389 if gpodder
.ui
.diablo
:
1390 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1391 log('All downloads have finished.', sender
=self
)
1392 if self
.config
.cmd_all_downloads_complete
:
1393 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1395 if gpodder
.ui
.fremantle
and failed
:
1396 message
= '\n'.join(['%s: %s' % (str(task
), \
1397 task
.error_message
) for task
in failed_downloads
])
1398 self
.show_message(message
, _('Downloads failed'), important
=True)
1400 # Remove finished episodes
1401 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1402 self
.cleanup_downloads()
1404 # Stop updating the download list here
1405 self
.download_list_update_enabled
= False
1407 if not gpodder
.ui
.fremantle
:
1408 self
.gPodder
.set_title(' - '.join(title
))
1410 self
.update_episode_list_icons(episode_urls
)
1411 if self
.episode_shownotes_window
is not None:
1412 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1413 shownotes_task
!= self
.episode_shownotes_window
.task
:
1414 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1415 self
.episode_shownotes_window
._download
_status
_progress
()
1416 self
.play_or_download()
1418 self
.update_podcast_list_model(channel_urls
)
1420 return self
.download_list_update_enabled
1421 except Exception, e
:
1422 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1423 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1424 # We return False here, so the update loop won't be called again,
1425 # that's why we require the restart of gPodder in the message.
1428 def on_config_changed(self
, *args
):
1429 util
.idle_add(self
._on
_config
_changed
, *args
)
1431 def _on_config_changed(self
, name
, old_value
, new_value
):
1432 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1433 self
.toolbar
.set_property('visible', new_value
)
1434 elif name
== 'videoplayer':
1435 self
.config
.video_played_dbus
= False
1436 elif name
== 'player':
1437 self
.config
.audio_played_dbus
= False
1438 elif name
== 'episode_list_descriptions':
1439 self
.update_episode_list_model()
1440 elif name
== 'episode_list_thumbnails':
1441 self
.update_episode_list_icons(all
=True)
1442 elif name
== 'rotation_mode':
1443 self
._fremantle
_rotation
.set_mode(new_value
)
1444 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1445 self
.restart_auto_update_timer()
1446 elif name
== 'podcast_list_view_all':
1447 # Force a update of the podcast list model
1448 self
.channel_list_changed
= True
1449 if gpodder
.ui
.fremantle
:
1450 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1451 while gtk
.events_pending():
1452 gtk
.main_iteration(False)
1453 self
.update_podcast_list_model()
1454 if gpodder
.ui
.fremantle
:
1455 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1457 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1458 # With get_bin_window, we get the window that contains the rows without
1459 # the header. The Y coordinate of this window will be the height of the
1460 # treeview header. This is the amount we have to subtract from the
1461 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1462 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1465 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1467 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
1468 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1471 if path
is not None:
1472 model
= treeview
.get_model()
1473 iter = model
.get_iter(path
)
1474 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1476 if role
== TreeViewHelper
.ROLE_EPISODES
:
1477 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1478 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1479 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1481 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1482 if last_tooltip
is not None and last_tooltip
!= id:
1483 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1485 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1487 if role
== TreeViewHelper
.ROLE_EPISODES
:
1488 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1490 tooltip
.set_text(description
)
1493 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1494 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1497 channel
.request_save_dir_size()
1498 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1499 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1501 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1502 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1503 table
= gtk
.Table(rows
=3, columns
=3)
1504 table
.set_row_spacings(5)
1505 table
.set_col_spacings(5)
1506 table
.set_border_width(5)
1508 heading
= gtk
.Label()
1509 heading
.set_alignment(0, 1)
1510 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1511 table
.attach(heading
, 0, 1, 0, 1)
1512 size_info
= gtk
.Label()
1513 size_info
.set_alignment(1, 1)
1514 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1515 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1516 table
.attach(size_info
, 2, 3, 0, 1)
1518 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1520 if len(channel
.description
) < 500:
1521 description
= channel
.description
1523 pos
= channel
.description
.find('\n\n')
1524 if pos
== -1 or pos
> 500:
1525 description
= channel
.description
[:498]+'[...]'
1527 description
= channel
.description
[:pos
]
1529 description
= gtk
.Label(description
)
1531 description
.set_markup(error_str
)
1532 description
.set_alignment(0, 0)
1533 description
.set_line_wrap(True)
1534 table
.attach(description
, 0, 3, 2, 3)
1537 tooltip
.set_custom(table
)
1541 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1544 def treeview_allow_tooltips(self
, treeview
, allow
):
1545 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1547 def update_m3u_playlist_clicked(self
, widget
):
1548 if self
.active_channel
is not None:
1549 self
.active_channel
.update_m3u_playlist()
1550 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1552 def treeview_handle_context_menu_click(self
, treeview
, event
):
1553 x
, y
= int(event
.x
), int(event
.y
)
1554 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1556 selection
= treeview
.get_selection()
1557 model
, paths
= selection
.get_selected_rows()
1559 if path
is None or (path
not in paths
and \
1560 event
.button
== self
.context_menu_mouse_button
):
1561 # We have right-clicked, but not into the selection,
1562 # assume we don't want to operate on the selection
1565 if path
is not None and not paths
and \
1566 event
.button
== self
.context_menu_mouse_button
:
1567 # No selection or clicked outside selection;
1568 # select the single item where we clicked
1569 treeview
.grab_focus()
1570 treeview
.set_cursor(path
, column
, 0)
1574 # Unselect any remaining items (clicked elsewhere)
1575 if hasattr(treeview
, 'is_rubber_banding_active'):
1576 if not treeview
.is_rubber_banding_active():
1577 selection
.unselect_all()
1579 selection
.unselect_all()
1583 def downloads_list_get_selection(self
, model
=None, paths
=None):
1584 if model
is None and paths
is None:
1585 selection
= self
.treeDownloads
.get_selection()
1586 model
, paths
= selection
.get_selected_rows()
1588 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1589 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1590 model
.get_value(model
.get_iter(path
), \
1591 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1593 for row_reference
, task
in selected_tasks
:
1594 if task
.status
!= download
.DownloadTask
.QUEUED
:
1596 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1597 download
.DownloadTask
.FAILED
, \
1598 download
.DownloadTask
.CANCELLED
):
1600 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1601 download
.DownloadTask
.QUEUED
, \
1602 download
.DownloadTask
.DOWNLOADING
, \
1603 download
.DownloadTask
.FAILED
):
1605 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1606 download
.DownloadTask
.DOWNLOADING
):
1608 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1609 download
.DownloadTask
.FAILED
, \
1610 download
.DownloadTask
.DONE
):
1613 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1615 def downloads_finished(self
, download_tasks_seen
):
1616 # FIXME: Filter all tasks that have already been reported
1617 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1618 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1620 if finished_downloads
and failed_downloads
:
1621 message
= self
.format_episode_list(finished_downloads
, 5)
1622 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1623 message
+= self
.format_episode_list(failed_downloads
, 5)
1624 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1625 elif finished_downloads
:
1626 message
= self
.format_episode_list(finished_downloads
)
1627 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1628 elif failed_downloads
:
1629 message
= self
.format_episode_list(failed_downloads
)
1630 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1632 # Open torrent files right after download (bug 1029)
1633 if self
.config
.open_torrent_after_download
:
1634 for task
in download_tasks_seen
:
1635 if task
.status
!= task
.DONE
:
1638 episode
= task
.episode
1639 if episode
.mimetype
!= 'application/x-bittorrent':
1642 self
.playback_episodes([episode
])
1645 def format_episode_list(self
, episode_list
, max_episodes
=10):
1647 Format a list of episode names for notifications
1649 Will truncate long episode names and limit the amount of
1650 episodes displayed (max_episodes=10).
1652 The episode_list parameter should be a list of strings.
1654 MAX_TITLE_LENGTH
= 100
1657 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1658 if len(title
) > MAX_TITLE_LENGTH
:
1659 middle
= (MAX_TITLE_LENGTH
/2)-2
1660 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1661 result
.append(saxutils
.escape(title
))
1664 more_episodes
= len(episode_list
) - max_episodes
1665 if more_episodes
> 0:
1666 result
.append('(...')
1667 result
.append(N_('%d more episode', '%d more episodes', more_episodes
) % more_episodes
)
1668 result
.append('...)')
1670 return (''.join(result
)).strip()
1672 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1673 episode_urls
= set()
1674 model
= self
.treeDownloads
.get_model()
1675 for row_reference
, task
in tasks
:
1676 if status
== download
.DownloadTask
.QUEUED
:
1677 # Only queue task when its paused/failed/cancelled (or forced)
1678 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1679 self
.download_queue_manager
.add_task(task
, force_start
)
1680 self
.enable_download_list_update()
1681 elif status
== download
.DownloadTask
.CANCELLED
:
1682 # Cancelling a download allowed when downloading/queued
1683 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1684 task
.status
= status
1685 # Cancelling paused/failed downloads requires a call to .run()
1686 elif task
.status
in (task
.PAUSED
, task
.FAILED
):
1687 task
.status
= status
1688 # Call run, so the partial file gets deleted
1690 elif status
== download
.DownloadTask
.PAUSED
:
1691 # Pausing a download only when queued/downloading
1692 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1693 task
.status
= status
1694 elif status
is None:
1695 # Remove the selected task - cancel downloading/queued tasks
1696 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1697 task
.status
= task
.CANCELLED
1698 model
.remove(model
.get_iter(row_reference
.get_path()))
1699 # Remember the URL, so we can tell the UI to update
1701 # We don't "see" this task anymore - remove it;
1702 # this is needed, so update_episode_list_icons()
1703 # below gets the correct list of "seen" tasks
1704 self
.download_tasks_seen
.remove(task
)
1705 except KeyError, key_error
:
1706 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1707 episode_urls
.add(task
.url
)
1708 # Tell the task that it has been removed (so it can clean up)
1709 task
.removed_from_list()
1711 # We can (hopefully) simply set the task status here
1712 task
.status
= status
1713 # Tell the podcasts tab to update icons for our removed podcasts
1714 self
.update_episode_list_icons(episode_urls
)
1715 # Update the tab title and downloads list
1716 self
.update_downloads_list()
1718 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1719 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1721 if not hasattr(treeview
, 'is_rubber_banding_active'):
1724 return not treeview
.is_rubber_banding_active()
1726 if event
.button
== self
.context_menu_mouse_button
:
1727 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1728 self
.downloads_list_get_selection(model
, paths
)
1730 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1731 # This creates a menu item for selection-wide actions
1732 item
= gtk
.ImageMenuItem(label
)
1733 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1734 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1735 item
.set_sensitive(sensitive
)
1736 return self
.set_finger_friendly(item
)
1740 item
= gtk
.ImageMenuItem(_('Episode details'))
1741 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1742 if len(selected_tasks
) == 1:
1743 row_reference
, task
= selected_tasks
[0]
1744 episode
= task
.episode
1745 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1747 item
.set_sensitive(False)
1748 menu
.append(self
.set_finger_friendly(item
))
1749 menu
.append(gtk
.SeparatorMenuItem())
1751 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1753 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1754 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1755 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1756 menu
.append(gtk
.SeparatorMenuItem())
1757 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1759 if gpodder
.ui
.maemo
:
1760 # Because we open the popup on left-click for Maemo,
1761 # we also include a non-action to close the menu
1762 menu
.append(gtk
.SeparatorMenuItem())
1763 item
= gtk
.ImageMenuItem(_('Close this menu'))
1764 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1766 menu
.append(self
.set_finger_friendly(item
))
1769 menu
.popup(None, None, None, event
.button
, event
.time
)
1772 def treeview_channels_show_context_menu(self
, treeview
, event
):
1773 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1777 # Check for valid channel id, if there's no id then
1778 # assume that it is a proxy channel or equivalent
1779 # and cannot be operated with right click
1780 if self
.active_channel
.id is None:
1783 if event
.button
== 3:
1788 item
= gtk
.ImageMenuItem( _('Update podcast'))
1789 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1790 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1791 item
.set_sensitive(not self
.updating_feed_cache
)
1794 menu
.append(gtk
.SeparatorMenuItem())
1796 item
= gtk
.CheckMenuItem(_('Keep episodes'))
1797 item
.set_active(self
.active_channel
.channel_is_locked
)
1798 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1799 menu
.append(self
.set_finger_friendly(item
))
1801 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1802 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1803 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1806 if self
.config
.device_type
!= 'none':
1807 item
= gtk
.MenuItem(_('Synchronize to device'))
1808 item
.connect('activate', lambda item
: self
.on_sync_to_ipod_activate(item
, self
.active_channel
.get_downloaded_episodes()))
1811 menu
.append( gtk
.SeparatorMenuItem())
1813 item
= gtk
.ImageMenuItem(_('Podcast details'))
1814 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1815 item
.connect('activate', self
.on_itemEditChannel_activate
)
1819 # Disable tooltips while we are showing the menu, so
1820 # the tooltip will not appear over the menu
1821 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1822 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1823 menu
.popup( None, None, None, event
.button
, event
.time
)
1827 def on_itemClose_activate(self
, widget
):
1828 if self
.tray_icon
is not None:
1829 self
.iconify_main_window()
1831 self
.on_gPodder_delete_event(widget
)
1833 def cover_file_removed(self
, channel_url
):
1835 The Cover Downloader calls this when a previously-
1836 available cover has been removed from the disk. We
1837 have to update our model to reflect this change.
1839 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1841 def cover_download_finished(self
, channel
, pixbuf
):
1843 The Cover Downloader calls this when it has finished
1844 downloading (or registering, if already downloaded)
1845 a new channel cover, which is ready for displaying.
1847 self
.podcast_list_model
.add_cover_by_channel(channel
, pixbuf
)
1849 def save_episodes_as_file(self
, episodes
):
1850 for episode
in episodes
:
1851 self
.save_episode_as_file(episode
)
1853 def save_episode_as_file(self
, episode
):
1854 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1855 if episode
.was_downloaded(and_exists
=True):
1856 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1857 copy_from
= episode
.local_filename(create
=False)
1858 assert copy_from
is not None
1859 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1860 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1861 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1863 def copy_episodes_bluetooth(self
, episodes
):
1864 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1866 if gpodder
.ui
.maemo
:
1867 util
.bluetooth_send_files_maemo([e
.local_filename(create
=False) \
1868 for e
in episodes_to_copy
])
1871 def convert_and_send_thread(episode
):
1872 for episode
in episodes
:
1873 filename
= episode
.local_filename(create
=False)
1874 assert filename
is not None
1875 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1876 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1877 (base
, ext
) = os
.path
.splitext(filename
)
1878 if not destfile
.endswith(ext
):
1882 shutil
.copyfile(filename
, destfile
)
1883 util
.bluetooth_send_file(destfile
)
1885 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1886 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1888 util
.delete_file(destfile
)
1890 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1892 def get_device_name(self
):
1893 if self
.config
.device_type
== 'ipod':
1895 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1896 return _('MP3 player')
1898 return '(unknown device)'
1900 def _treeview_button_released(self
, treeview
, event
):
1901 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1902 dy
= int(abs(event
.y
-ypos
))
1903 dx
= int(event
.x
-xpos
)
1905 selection
= treeview
.get_selection()
1906 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1907 if path
is None or dy
> 30:
1908 return (False, dx
, dy
)
1910 path
, column
, x
, y
= path
1911 selection
.select_path(path
)
1912 treeview
.set_cursor(path
)
1913 treeview
.grab_focus()
1915 return (True, dx
, dy
)
1917 def treeview_channels_handle_gestures(self
, treeview
, event
):
1918 if self
.currently_updating
:
1921 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1924 if self
.config
.maemo_enable_gestures
:
1926 self
.on_itemUpdateChannel_activate()
1928 self
.on_itemEditChannel_activate(treeview
)
1932 def treeview_available_handle_gestures(self
, treeview
, event
):
1933 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1936 if self
.config
.maemo_enable_gestures
:
1938 self
.on_playback_selected_episodes(None)
1941 self
.on_shownotes_selected_episodes(None)
1944 # Pass the event to the context menu handler for treeAvailable
1945 self
.treeview_available_show_context_menu(treeview
, event
)
1949 def treeview_available_show_context_menu(self
, treeview
, event
):
1950 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1952 if not hasattr(treeview
, 'is_rubber_banding_active'):
1955 return not treeview
.is_rubber_banding_active()
1957 if event
.button
== self
.context_menu_mouse_button
:
1958 episodes
= self
.get_selected_episodes()
1959 any_locked
= any(e
.is_locked
for e
in episodes
)
1960 any_played
= any(e
.is_played
for e
in episodes
)
1961 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1962 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1963 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
1967 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1969 if open_instead_of_play
:
1970 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1972 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1974 item
= gtk
.ImageMenuItem(_('Stream'))
1975 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
1977 item
.set_sensitive(can_play
and not downloading
)
1978 item
.connect('activate', self
.on_playback_selected_episodes
)
1979 menu
.append(self
.set_finger_friendly(item
))
1982 item
= gtk
.ImageMenuItem(_('Download'))
1983 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1984 item
.set_sensitive(can_download
)
1985 item
.connect('activate', self
.on_download_selected_episodes
)
1986 menu
.append(self
.set_finger_friendly(item
))
1988 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1989 item
.connect('activate', self
.on_item_cancel_download_activate
)
1990 menu
.append(self
.set_finger_friendly(item
))
1992 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1993 item
.set_sensitive(can_delete
)
1994 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1995 menu
.append(self
.set_finger_friendly(item
))
1999 # Ok, this probably makes sense to only display for downloaded files
2001 menu
.append(gtk
.SeparatorMenuItem())
2002 share_item
= gtk
.MenuItem(_('Send to'))
2003 menu
.append(self
.set_finger_friendly(share_item
))
2004 share_menu
= gtk
.Menu()
2006 item
= gtk
.ImageMenuItem(_('Local folder'))
2007 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
2008 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
2009 share_menu
.append(self
.set_finger_friendly(item
))
2010 if self
.bluetooth_available
:
2011 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
2012 if gpodder
.ui
.maemo
:
2013 icon_name
= ICON('qgn_list_filesys_bluetooth')
2015 icon_name
= ICON('bluetooth')
2016 item
.set_image(gtk
.image_new_from_icon_name(icon_name
, gtk
.ICON_SIZE_MENU
))
2017 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
2018 share_menu
.append(self
.set_finger_friendly(item
))
2020 item
= gtk
.ImageMenuItem(self
.get_device_name())
2021 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
2022 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
2023 share_menu
.append(self
.set_finger_friendly(item
))
2025 share_item
.set_submenu(share_menu
)
2027 if (downloaded
or one_is_new
or can_download
) and not downloading
:
2028 menu
.append(gtk
.SeparatorMenuItem())
2030 item
= gtk
.CheckMenuItem(_('New'))
2031 item
.set_active(True)
2032 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
2033 menu
.append(self
.set_finger_friendly(item
))
2035 item
= gtk
.CheckMenuItem(_('New'))
2036 item
.set_active(False)
2037 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
2038 menu
.append(self
.set_finger_friendly(item
))
2041 item
= gtk
.CheckMenuItem(_('Played'))
2042 item
.set_active(any_played
)
2043 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
2044 menu
.append(self
.set_finger_friendly(item
))
2046 item
= gtk
.CheckMenuItem(_('Keep episode'))
2047 item
.set_active(any_locked
)
2048 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
2049 menu
.append(self
.set_finger_friendly(item
))
2051 menu
.append(gtk
.SeparatorMenuItem())
2052 # Single item, add episode information menu item
2053 item
= gtk
.ImageMenuItem(_('Episode details'))
2054 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
2055 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
2056 menu
.append(self
.set_finger_friendly(item
))
2058 if gpodder
.ui
.maemo
:
2059 # Because we open the popup on left-click for Maemo,
2060 # we also include a non-action to close the menu
2061 menu
.append(gtk
.SeparatorMenuItem())
2062 item
= gtk
.ImageMenuItem(_('Close this menu'))
2063 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
2064 menu
.append(self
.set_finger_friendly(item
))
2067 # Disable tooltips while we are showing the menu, so
2068 # the tooltip will not appear over the menu
2069 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
2070 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2071 menu
.popup( None, None, None, event
.button
, event
.time
)
2075 def set_title(self
, new_title
):
2076 if not gpodder
.ui
.fremantle
:
2077 self
.default_title
= new_title
2078 self
.gPodder
.set_title(new_title
)
2080 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2082 Updates the status icons in the episode list.
2084 If urls is given, it should be a list of URLs
2085 of episodes that should be updated.
2087 If urls is None, set ONE OF selected, all to
2088 True (the former updates just the selected
2089 episodes and the latter updates all episodes).
2091 additional_args
= (self
.episode_is_downloading
, \
2092 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2093 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2095 if urls
is not None:
2096 # We have a list of URLs to walk through
2097 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2098 elif selected
and not all
:
2099 # We should update all selected episodes
2100 selection
= self
.treeAvailable
.get_selection()
2101 model
, paths
= selection
.get_selected_rows()
2102 for path
in reversed(paths
):
2103 iter = model
.get_iter(path
)
2104 self
.episode_list_model
.update_by_filter_iter(iter, \
2106 elif all
and not selected
:
2107 # We update all (even the filter-hidden) episodes
2108 self
.episode_list_model
.update_all(*additional_args
)
2110 # Wrong/invalid call - have to specify at least one parameter
2111 raise ValueError('Invalid call to update_episode_list_icons')
2113 def episode_list_status_changed(self
, episodes
):
2114 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2115 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2118 def clean_up_downloads(self
, delete_partial
=False):
2119 # Clean up temporary files left behind by old gPodder versions
2120 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2123 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2125 for tempfile
in temporary_files
:
2126 util
.delete_file(tempfile
)
2128 # Clean up empty download folders and abandoned download folders
2129 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2130 for ddir
in download_dirs
:
2131 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2132 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2133 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2134 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2135 shutil
.rmtree(ddir
, ignore_errors
=True)
2137 def streaming_possible(self
):
2138 if gpodder
.ui
.desktop
:
2139 # User has to have a media player set on the Desktop, or else we
2140 # would probably open the browser when giving a URL to xdg-open..
2141 return (self
.config
.player
and self
.config
.player
!= 'default')
2142 elif gpodder
.ui
.maemo
:
2143 # On Maemo, the default is to use the Nokia Media Player, which is
2144 # already able to deal with HTTP URLs the right way, so we
2145 # unconditionally enable streaming always on Maemo
2150 def playback_episodes_for_real(self
, episodes
):
2151 groups
= collections
.defaultdict(list)
2152 for episode
in episodes
:
2153 file_type
= episode
.file_type()
2154 if file_type
== 'video' and self
.config
.videoplayer
and \
2155 self
.config
.videoplayer
!= 'default':
2156 player
= self
.config
.videoplayer
2157 if gpodder
.ui
.diablo
:
2158 # Use the wrapper script if it's installed to crop 3GP YouTube
2159 # videos to fit the screen (looks much nicer than w/ black border)
2160 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2161 player
= 'gpodder-mplayer'
2162 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2163 player
= 'mplayer -fs %F'
2164 elif file_type
== 'audio' and self
.config
.player
and \
2165 self
.config
.player
!= 'default':
2166 player
= self
.config
.player
2170 if file_type
not in ('audio', 'video') or \
2171 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2172 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2173 # Mark episode as played in the database
2174 episode
.mark(is_played
=True)
2175 self
.mygpo_client
.on_playback([episode
])
2177 filename
= episode
.local_filename(create
=False)
2178 if filename
is None or not os
.path
.exists(filename
):
2179 filename
= episode
.url
2180 if youtube
.is_video_link(filename
):
2181 fmt_id
= self
.config
.youtube_preferred_fmt_id
2182 if gpodder
.ui
.fremantle
:
2184 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2186 # Determine the playback resume position - if the file
2187 # was played 100%, we simply start from the beginning
2188 resume_position
= episode
.current_position
2189 if resume_position
== episode
.total_time
:
2192 if gpodder
.ui
.fremantle
:
2193 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2195 # If Panucci is configured, use D-Bus on Maemo to call it
2196 if player
== 'panucci':
2198 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2199 PANUCCI_PATH
= '/panucciInterface'
2200 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2201 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2202 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2204 def on_reply(*args
):
2207 def error_handler(filename
, err
):
2208 log('Exception in D-Bus call: %s', str(err
), \
2211 # Fallback: use the command line client
2212 for command
in util
.format_desktop_command('panucci', \
2214 log('Executing: %s', repr(command
), sender
=self
)
2215 subprocess
.Popen(command
)
2217 on_error
= lambda err
: error_handler(filename
, err
)
2219 # This method only exists in Panucci > 0.9 ('new Panucci')
2220 i
.playback_from(filename
, resume_position
, \
2221 reply_handler
=on_reply
, error_handler
=on_error
)
2223 continue # This file was handled by the D-Bus call
2224 except Exception, e
:
2225 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2226 elif player
== 'MediaBox' and gpodder
.ui
.maemo
:
2228 MEDIABOX_NAME
= 'de.pycage.mediabox'
2229 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2230 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2231 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2232 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2234 def on_reply(*args
):
2238 log('Exception in D-Bus call: %s', str(err
), \
2241 i
.load(filename
, '%s/x-unknown' % file_type
, \
2242 reply_handler
=on_reply
, error_handler
=on_error
)
2244 continue # This file was handled by the D-Bus call
2245 except Exception, e
:
2246 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2248 groups
[player
].append(filename
)
2250 # Open episodes with system default player
2251 if 'default' in groups
:
2252 if gpodder
.ui
.maemo
and len(groups
['default']) > 1:
2253 # The Nokia Media Player app does not support receiving multiple
2254 # file names via D-Bus, so we simply place all file names into a
2255 # temporary M3U playlist and open that with the Media Player.
2256 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2257 util
.write_m3u_playlist(m3u_filename
, groups
['default'], extm3u
=False)
2258 util
.gui_open(m3u_filename
)
2260 for filename
in groups
['default']:
2261 log('Opening with system default: %s', filename
, sender
=self
)
2262 util
.gui_open(filename
)
2263 del groups
['default']
2264 elif gpodder
.ui
.maemo
and groups
:
2265 # When on Maemo and not opening with default, show a notification
2266 # (no startup notification for Panucci / MPlayer yet...)
2267 if len(episodes
) == 1:
2268 text
= _('Opening %s') % episodes
[0].title
2270 count
= len(episodes
)
2271 text
= N_('Opening %d episode', 'Opening %d episodes', count
) % count
2273 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2275 def destroy_banner_later(banner
):
2278 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2280 # For each type now, go and create play commands
2281 for group
in groups
:
2282 for command
in util
.format_desktop_command(group
, groups
[group
]):
2283 log('Executing: %s', repr(command
), sender
=self
)
2284 subprocess
.Popen(command
)
2286 # Persist episode status changes to the database
2289 # Flush updated episode status
2290 self
.mygpo_client
.flush()
2292 def playback_episodes(self
, episodes
):
2293 # We need to create a list, because we run through it more than once
2294 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2295 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2298 self
.playback_episodes_for_real(episodes
)
2299 except Exception, e
:
2300 log('Error in playback!', sender
=self
, traceback
=True)
2301 if gpodder
.ui
.desktop
:
2302 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2303 _('Error opening player'), widget
=self
.toolPreferences
)
2305 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2307 channel_urls
= set()
2308 episode_urls
= set()
2309 for episode
in episodes
:
2310 channel_urls
.add(episode
.channel
.url
)
2311 episode_urls
.add(episode
.url
)
2312 self
.update_episode_list_icons(episode_urls
)
2313 self
.update_podcast_list_model(channel_urls
)
2315 def play_or_download(self
):
2316 if not gpodder
.ui
.fremantle
:
2317 if self
.wNotebook
.get_current_page() > 0:
2318 if gpodder
.ui
.desktop
:
2319 self
.toolCancel
.set_sensitive(True)
2322 if self
.currently_updating
:
2323 return (False, False, False, False, False, False)
2325 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2326 ( is_played
, is_locked
) = (False,)*2
2328 open_instead_of_play
= False
2330 selection
= self
.treeAvailable
.get_selection()
2331 if selection
.count_selected_rows() > 0:
2332 (model
, paths
) = selection
.get_selected_rows()
2336 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2337 except TypeError, te
:
2338 log('Invalid episode at path %s', str(path
), sender
=self
)
2341 if episode
.file_type() not in ('audio', 'video'):
2342 open_instead_of_play
= True
2344 if episode
.was_downloaded():
2345 can_play
= episode
.was_downloaded(and_exists
=True)
2346 is_played
= episode
.is_played
2347 is_locked
= episode
.is_locked
2351 if self
.episode_is_downloading(episode
):
2356 can_download
= can_download
and not can_cancel
2357 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2358 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2359 can_delete
= not can_cancel
2361 if gpodder
.ui
.desktop
:
2362 if open_instead_of_play
:
2363 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2365 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2366 self
.toolPlay
.set_sensitive( can_play
)
2367 self
.toolDownload
.set_sensitive( can_download
)
2368 self
.toolTransfer
.set_sensitive( can_transfer
)
2369 self
.toolCancel
.set_sensitive( can_cancel
)
2371 if not gpodder
.ui
.fremantle
:
2372 self
.item_cancel_download
.set_sensitive(can_cancel
)
2373 self
.itemDownloadSelected
.set_sensitive(can_download
)
2374 self
.itemOpenSelected
.set_sensitive(can_play
)
2375 self
.itemPlaySelected
.set_sensitive(can_play
)
2376 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2377 self
.item_toggle_played
.set_sensitive(can_play
)
2378 self
.item_toggle_lock
.set_sensitive(can_play
)
2379 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2380 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2382 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2384 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2385 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2387 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2388 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2390 def episode_new_status_changed(self
, urls
):
2391 self
.update_podcast_list_model()
2392 self
.update_episode_list_icons(urls
)
2394 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2395 """Update the podcast list treeview model
2397 If urls is given, it should list the URLs of each
2398 podcast that has to be updated in the list.
2400 If selected is True, only update the model contents
2401 for the currently-selected podcast - nothing more.
2403 The caller can optionally specify "select_url",
2404 which is the URL of the podcast that is to be
2405 selected in the list after the update is complete.
2406 This only works if the podcast list has to be
2407 reloaded; i.e. something has been added or removed
2408 since the last update of the podcast list).
2410 selection
= self
.treeChannels
.get_selection()
2411 model
, iter = selection
.get_selected()
2413 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2414 # Update "all episodes" view in any case (if enabled)
2415 self
.podcast_list_model
.update_first_row()
2418 # very cheap! only update selected channel
2419 if iter is not None:
2420 # If we have selected the "all episodes" view, we have
2421 # to update all channels for selected episodes:
2422 if self
.config
.podcast_list_view_all
and \
2423 self
.podcast_list_model
.iter_is_first_row(iter):
2424 urls
= self
.get_podcast_urls_from_selected_episodes()
2425 self
.podcast_list_model
.update_by_urls(urls
)
2427 # Otherwise just update the selected row (a podcast)
2428 self
.podcast_list_model
.update_by_filter_iter(iter)
2429 elif not self
.channel_list_changed
:
2430 # we can keep the model, but have to update some
2432 # still cheaper than reloading the whole list
2433 self
.podcast_list_model
.update_all()
2435 # ok, we got a bunch of urls to update
2436 self
.podcast_list_model
.update_by_urls(urls
)
2438 if model
and iter and select_url
is None:
2439 # Get the URL of the currently-selected podcast
2440 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2442 # Update the podcast list model with new channels
2443 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2446 selected_iter
= model
.get_iter_first()
2447 # Find the previously-selected URL in the new
2448 # model if we have an URL (else select first)
2449 if select_url
is not None:
2450 pos
= model
.get_iter_first()
2451 while pos
is not None:
2452 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2453 if url
== select_url
:
2456 pos
= model
.iter_next(pos
)
2458 if not gpodder
.ui
.fremantle
:
2459 if selected_iter
is not None:
2460 selection
.select_iter(selected_iter
)
2461 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2463 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2464 self
.channel_list_changed
= False
2466 def episode_is_downloading(self
, episode
):
2467 """Returns True if the given episode is being downloaded at the moment"""
2471 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2473 def update_episode_list_model(self
):
2474 if self
.channels
and self
.active_channel
is not None:
2475 if gpodder
.ui
.fremantle
:
2476 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2478 self
.currently_updating
= True
2479 self
.episode_list_model
.clear()
2480 self
.episode_list_model
.reset_update_progress()
2481 self
.treeAvailable
.set_model(self
.empty_episode_list_model
)
2482 def do_update_episode_list_model():
2483 additional_args
= (self
.episode_is_downloading
, \
2484 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2485 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
, \
2487 self
.episode_list_model
.add_from_channel(self
.active_channel
, *additional_args
)
2489 def on_episode_list_model_updated():
2490 if gpodder
.ui
.fremantle
:
2491 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2492 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
2493 self
.treeAvailable
.columns_autosize()
2494 self
.currently_updating
= False
2495 self
.play_or_download()
2496 util
.idle_add(on_episode_list_model_updated
)
2497 threading
.Thread(target
=do_update_episode_list_model
).start()
2499 self
.episode_list_model
.clear()
2501 @dbus.service
.method(gpodder
.dbus_interface
)
2502 def offer_new_episodes(self
, channels
=None):
2503 if gpodder
.ui
.fremantle
:
2504 # Assume that when this function is called that the
2505 # notification is not shown anymore (Maemo bug 11345)
2506 self
._fremantle
_notification
_visible
= False
2508 new_episodes
= self
.get_new_episodes(channels
)
2510 self
.new_episodes_show(new_episodes
)
2514 def add_podcast_list(self
, urls
, auth_tokens
=None):
2515 """Subscribe to a list of podcast given their URLs
2517 If auth_tokens is given, it should be a dictionary
2518 mapping URLs to (username, password) tuples."""
2520 if auth_tokens
is None:
2523 # Sort and split the URL list into five buckets
2524 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2525 for input_url
in urls
:
2526 url
= util
.normalize_feed_url(input_url
)
2528 # Fail this one because the URL is not valid
2529 failed
.append(input_url
)
2530 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2531 # A podcast already exists in the list for this URL
2532 existing
.append(url
)
2534 # This URL has survived the first round - queue for add
2536 if url
!= input_url
and input_url
in auth_tokens
:
2537 auth_tokens
[url
] = auth_tokens
[input_url
]
2542 progress
= ProgressIndicator(_('Adding podcasts'), \
2543 _('Please wait while episode information is downloaded.'), \
2544 parent
=self
.get_dialog_parent())
2546 def on_after_update():
2547 progress
.on_finished()
2548 # Report already-existing subscriptions to the user
2550 title
= _('Existing subscriptions skipped')
2551 message
= _('You are already subscribed to these podcasts:') \
2552 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2553 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2555 # Report subscriptions that require authentication
2559 title
= _('Podcast requires authentication')
2560 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2561 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2563 retry_podcasts
[url
] = auth_tokens
2565 # Stop asking the user for more login data
2568 error_messages
[url
] = _('Authentication failed')
2572 # If we have authentication data to retry, do so here
2574 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2576 # Report website redirections
2577 for url
in redirections
:
2578 title
= _('Website redirection detected')
2579 message
= _('The URL %(url)s redirects to %(target)s.') \
2580 + '\n\n' + _('Do you want to visit the website now?')
2581 message
= message
% {'url': url
, 'target': redirections
[url
]}
2582 if self
.show_confirmation(message
, title
):
2583 util
.open_website(url
)
2587 # Report failed subscriptions to the user
2589 title
= _('Could not add some podcasts')
2590 message
= _('Some podcasts could not be added to your list:') \
2591 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2592 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2593 self
.show_message(message
, title
, important
=True)
2595 # Upload subscription changes to gpodder.net
2596 self
.mygpo_client
.on_subscribe(worked
)
2598 # If at least one podcast has been added, save and update all
2599 if self
.channel_list_changed
:
2600 # Fix URLs if mygpo has rewritten them
2601 self
.rewrite_urls_mygpo()
2603 self
.save_channels_opml()
2605 # If only one podcast was added, select it after the update
2606 if len(worked
) == 1:
2611 # Update the list of subscribed podcasts
2612 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2613 self
.update_podcasts_tab()
2615 # Offer to download new episodes
2617 for podcast
in self
.channels
:
2618 if podcast
.url
in worked
:
2619 episodes
.extend(podcast
.get_all_episodes())
2622 episodes
= list(PodcastEpisode
.sort_by_pubdate(episodes
, \
2624 self
.new_episodes_show(episodes
, \
2625 selected
=[e
.check_is_new() for e
in episodes
])
2629 # After the initial sorting and splitting, try all queued podcasts
2630 length
= len(queued
)
2631 for index
, url
in enumerate(queued
):
2632 progress
.on_progress(float(index
)/float(length
))
2633 progress
.on_message(url
)
2634 log('QUEUE RUNNER: %s', url
, sender
=self
)
2636 # The URL is valid and does not exist already - subscribe!
2637 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2638 authentication_tokens
=auth_tokens
.get(url
, None), \
2639 max_episodes
=self
.config
.max_episodes_per_feed
, \
2640 download_dir
=self
.config
.download_dir
, \
2641 allow_empty_feeds
=self
.config
.allow_empty_feeds
, \
2642 mimetype_prefs
=self
.config
.mimetype_prefs
)
2645 username
, password
= util
.username_password_from_url(url
)
2646 except ValueError, ve
:
2647 username
, password
= (None, None)
2649 if username
is not None and channel
.username
is None and \
2650 password
is not None and channel
.password
is None:
2651 channel
.username
= username
2652 channel
.password
= password
2655 self
._update
_cover
(channel
)
2656 except feedcore
.AuthenticationRequired
:
2657 if url
in auth_tokens
:
2658 # Fail for wrong authentication data
2659 error_messages
[url
] = _('Authentication failed')
2662 # Queue for login dialog later
2665 except feedcore
.WifiLogin
, error
:
2666 redirections
[url
] = error
.data
2668 error_messages
[url
] = _('Redirection detected')
2670 except Exception, e
:
2671 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2672 error_messages
[url
] = str(e
)
2676 assert channel
is not None
2677 worked
.append(channel
.url
)
2678 self
.channels
.append(channel
)
2679 self
.channel_list_changed
= True
2680 util
.idle_add(on_after_update
)
2681 threading
.Thread(target
=thread_proc
).start()
2683 def save_channels_opml(self
):
2684 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2685 return exporter
.write(self
.channels
)
2687 def find_episode(self
, podcast_url
, episode_url
):
2688 """Find an episode given its podcast and episode URL
2690 The function will return a PodcastEpisode object if
2691 the episode is found, or None if it's not found.
2693 for podcast
in self
.channels
:
2694 if podcast_url
== podcast
.url
:
2695 for episode
in podcast
.get_all_episodes():
2696 if episode_url
== episode
.url
:
2701 def process_received_episode_actions(self
, updated_urls
):
2702 """Process/merge episode actions from gpodder.net
2704 This function will merge all changes received from
2705 the server to the local database and update the
2706 status of the affected episodes as necessary.
2708 indicator
= ProgressIndicator(_('Merging episode actions'), \
2709 _('Episode actions from gpodder.net are merged.'), \
2710 False, self
.get_dialog_parent())
2712 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2713 if action
.action
== 'play':
2714 episode
= self
.find_episode(action
.podcast_url
, \
2717 if episode
is not None:
2718 log('Play action for %s', episode
.url
, sender
=self
)
2719 episode
.mark(is_played
=True)
2721 if action
.timestamp
> episode
.current_position_updated
and \
2722 action
.position
is not None:
2723 log('Updating position for %s', episode
.url
, sender
=self
)
2724 episode
.current_position
= action
.position
2725 episode
.current_position_updated
= action
.timestamp
2728 log('Updating total time for %s', episode
.url
, sender
=self
)
2729 episode
.total_time
= action
.total
2732 elif action
.action
== 'delete':
2733 episode
= self
.find_episode(action
.podcast_url
, \
2736 if episode
is not None:
2737 if not episode
.was_downloaded(and_exists
=True):
2738 # Set the episode to a "deleted" state
2739 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2740 episode
.delete_from_disk()
2743 indicator
.on_message(N_('%d action processed', '%d actions processed', idx
) % idx
)
2744 gtk
.main_iteration(False)
2746 indicator
.on_finished()
2750 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2752 self
.updating_feed_cache
= False
2754 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2756 # Process received episode actions for all updated URLs
2757 self
.process_received_episode_actions(updated_urls
)
2759 self
.channel_list_changed
= True
2760 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2762 # Only search for new episodes in podcasts that have been
2763 # updated, not in other podcasts (for single-feed updates)
2764 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2766 if gpodder
.ui
.fremantle
:
2767 self
.fancy_progress_bar
.hide()
2768 self
.button_subscribe
.set_sensitive(True)
2769 self
.button_refresh
.set_sensitive(True)
2770 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2771 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2772 self
.update_podcasts_tab()
2773 self
.update_episode_list_model()
2774 if self
.feed_cache_update_cancelled
:
2777 def application_in_foreground():
2779 return any(w
.get_property('is-topmost') for w
in hildon
.WindowStack
.get_default().get_windows())
2780 except Exception, e
:
2781 log('Could not determine is-topmost', traceback
=True)
2782 # When in doubt, assume not in foreground
2786 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2787 # New episodes found, but we should do nothing
2788 self
.show_message(_('New episodes are available.'))
2789 elif self
.config
.auto_download
== 'always':
2790 count
= len(episodes
)
2791 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2792 self
.show_message(title
)
2793 self
.download_episode_list(episodes
)
2794 elif self
.config
.auto_download
== 'queue':
2795 self
.show_message(_('New episodes have been added to the download list.'))
2796 self
.download_episode_list_paused(episodes
)
2797 elif application_in_foreground():
2798 if not self
._fremantle
_notification
_visible
:
2799 self
.new_episodes_show(episodes
)
2800 elif not self
._fremantle
_notification
_visible
:
2803 pynotify
.init('gPodder')
2804 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2805 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2806 n
.set_hint('dbus-callback-default', ' '.join([
2807 gpodder
.dbus_bus_name
,
2808 gpodder
.dbus_gui_object_path
,
2809 gpodder
.dbus_interface
,
2810 'offer_new_episodes',
2812 n
.set_category('gpodder-new-episodes')
2814 self
._fremantle
_notification
_visible
= True
2815 except Exception, e
:
2816 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2817 self
.new_episodes_show(episodes
)
2818 self
._fremantle
_notification
_visible
= False
2819 elif not self
.config
.auto_update_feeds
:
2820 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2824 self
.tray_icon
.set_status()
2826 if self
.feed_cache_update_cancelled
:
2827 # The user decided to abort the feed update
2828 self
.show_update_feeds_buttons()
2830 # Nothing new here - but inform the user
2831 self
.pbFeedUpdate
.set_fraction(1.0)
2832 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2833 self
.feed_cache_update_cancelled
= True
2834 self
.btnCancelFeedUpdate
.show()
2835 self
.btnCancelFeedUpdate
.set_sensitive(True)
2836 self
.itemUpdate
.set_sensitive(True)
2837 if gpodder
.ui
.maemo
:
2838 # btnCancelFeedUpdate is a ToolButton on Maemo
2839 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2841 # btnCancelFeedUpdate is a normal gtk.Button
2842 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2844 count
= len(episodes
)
2845 # New episodes are available
2846 self
.pbFeedUpdate
.set_fraction(1.0)
2847 # Are we minimized and should we auto download?
2848 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2849 self
.download_episode_list(episodes
)
2850 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2851 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2852 self
.show_update_feeds_buttons()
2853 elif self
.config
.auto_download
== 'queue':
2854 self
.download_episode_list_paused(episodes
)
2855 title
= N_('%d new episode added to download list.', '%d new episodes added to download list.', count
) % count
2856 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2857 self
.show_update_feeds_buttons()
2859 self
.show_update_feeds_buttons()
2860 # New episodes are available and we are not minimized
2861 if not self
.config
.do_not_show_new_episodes_dialog
:
2862 self
.new_episodes_show(episodes
, notification
=True)
2864 message
= N_('%d new episode available', '%d new episodes available', count
) % count
2865 self
.pbFeedUpdate
.set_text(message
)
2867 def _update_cover(self
, channel
):
2868 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2869 self
.cover_downloader
.request_cover(channel
)
2871 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2872 total
= len(channels
)
2874 for updated
, channel
in enumerate(channels
):
2875 if not self
.feed_cache_update_cancelled
:
2877 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
, \
2878 mimetype_prefs
=self
.config
.mimetype_prefs
)
2879 self
._update
_cover
(channel
)
2880 except Exception, e
:
2881 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2883 message
= _('Error while updating %(url)s: %(message)s')
2885 message
= _('The feed at %(url)s could not be updated.')
2886 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2887 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2889 if self
.feed_cache_update_cancelled
:
2892 # By the time we get here the update may have already been cancelled
2893 if not self
.feed_cache_update_cancelled
:
2894 def update_progress():
2895 d
= {'podcast': channel
.title
, 'position': updated
+1, 'total': total
}
2896 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2897 self
.pbFeedUpdate
.set_text(progression
)
2899 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2900 self
.pbFeedUpdate
.set_fraction(float(updated
+1)/float(total
))
2901 util
.idle_add(update_progress
)
2903 updated_urls
= [c
.url
for c
in channels
]
2904 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2906 def show_update_feeds_buttons(self
):
2907 # Make sure that the buttons for updating feeds
2908 # appear - this should happen after a feed update
2909 if gpodder
.ui
.maemo
:
2910 self
.btnUpdateSelectedFeed
.show()
2911 self
.toolFeedUpdateProgress
.hide()
2912 self
.btnCancelFeedUpdate
.hide()
2913 self
.btnCancelFeedUpdate
.set_is_important(False)
2914 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2915 self
.toolbarSpacer
.set_expand(True)
2916 self
.toolbarSpacer
.set_draw(False)
2918 self
.hboxUpdateFeeds
.hide()
2919 self
.btnUpdateFeeds
.show()
2920 self
.itemUpdate
.set_sensitive(True)
2921 self
.itemUpdateChannel
.set_sensitive(True)
2923 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2924 if not self
.feed_cache_update_cancelled
:
2925 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2926 self
.feed_cache_update_cancelled
= True
2927 if not gpodder
.ui
.fremantle
:
2928 self
.btnCancelFeedUpdate
.set_sensitive(False)
2929 elif not gpodder
.ui
.fremantle
:
2930 self
.show_update_feeds_buttons()
2932 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2933 if self
.updating_feed_cache
:
2934 if gpodder
.ui
.fremantle
:
2935 self
.feed_cache_update_cancelled
= True
2938 if not force_update
:
2939 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2940 self
.channel_list_changed
= True
2941 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2944 # Fix URLs if mygpo has rewritten them
2945 self
.rewrite_urls_mygpo()
2947 self
.updating_feed_cache
= True
2949 if channels
is None:
2950 # Only update podcasts for which updates are enabled
2951 channels
= [c
for c
in self
.channels
if c
.feed_update_enabled
]
2953 if gpodder
.ui
.fremantle
:
2954 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2955 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2956 self
.fancy_progress_bar
.show()
2957 self
.button_subscribe
.set_sensitive(False)
2958 self
.button_refresh
.set_sensitive(False)
2959 self
.feed_cache_update_cancelled
= False
2961 self
.itemUpdate
.set_sensitive(False)
2962 self
.itemUpdateChannel
.set_sensitive(False)
2965 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2967 self
.feed_cache_update_cancelled
= False
2968 self
.btnCancelFeedUpdate
.show()
2969 self
.btnCancelFeedUpdate
.set_sensitive(True)
2970 if gpodder
.ui
.maemo
:
2971 self
.toolbarSpacer
.set_expand(False)
2972 self
.toolbarSpacer
.set_draw(True)
2973 self
.btnUpdateSelectedFeed
.hide()
2974 self
.toolFeedUpdateProgress
.show_all()
2976 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2977 self
.hboxUpdateFeeds
.show_all()
2978 self
.btnUpdateFeeds
.hide()
2980 if len(channels
) == 1:
2981 text
= _('Updating "%s"...') % channels
[0].title
2983 count
= len(channels
)
2984 text
= N_('Updating %d feed...', 'Updating %d feeds...', count
) % count
2985 self
.pbFeedUpdate
.set_text(text
)
2986 self
.pbFeedUpdate
.set_fraction(0)
2988 args
= (channels
, select_url_afterwards
)
2989 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2991 def on_gPodder_delete_event(self
, widget
, *args
):
2992 """Called when the GUI wants to close the window
2993 Displays a confirmation dialog (and closes/hides gPodder)
2996 downloading
= self
.download_status_model
.are_downloads_in_progress()
2998 # Only iconify if we are using the window's "X" button,
2999 # but not when we are using "Quit" in the menu or toolbar
3000 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
3001 self
.iconify_main_window()
3002 elif self
.config
.on_quit_ask
or downloading
:
3003 if gpodder
.ui
.fremantle
:
3004 self
.close_gpodder()
3005 elif gpodder
.ui
.diablo
:
3006 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
3008 self
.close_gpodder()
3011 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
3012 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3013 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
3015 title
= _('Quit gPodder')
3017 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3019 message
= _('Do you really want to quit gPodder now?')
3021 dialog
.set_title(title
)
3022 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
3024 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
3025 dialog
.vbox
.pack_start(cb_ask
)
3028 quit_button
.grab_focus()
3029 result
= dialog
.run()
3032 if result
== gtk
.RESPONSE_CLOSE
:
3033 if not downloading
and cb_ask
.get_active() == True:
3034 self
.config
.on_quit_ask
= False
3035 self
.close_gpodder()
3037 self
.close_gpodder()
3041 def close_gpodder(self
):
3042 """ clean everything and exit properly
3045 if self
.save_channels_opml():
3046 pass # FIXME: Add mygpo synchronization here
3048 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
3052 if self
.tray_icon
is not None:
3053 self
.tray_icon
.set_visible(False)
3055 # Notify all tasks to to carry out any clean-up actions
3056 self
.download_status_model
.tell_all_tasks_to_quit()
3058 while gtk
.events_pending():
3059 gtk
.main_iteration(False)
3066 def get_expired_episodes(self
):
3067 for channel
in self
.channels
:
3068 for episode
in channel
.get_downloaded_episodes():
3069 # Never consider locked episodes as old
3070 if episode
.is_locked
:
3073 # Never consider fresh episodes as old
3074 if episode
.age_in_days() < self
.config
.episode_old_age
:
3077 # Do not delete played episodes (except if configured)
3078 if episode
.is_played
:
3079 if not self
.config
.auto_remove_played_episodes
:
3082 # Do not delete unplayed episodes (except if configured)
3083 if not episode
.is_played
:
3084 if not self
.config
.auto_remove_unplayed_episodes
:
3089 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3094 episodes
= [e
for e
in episodes
if not e
.is_locked
]
3097 title
= _('Episodes are locked')
3098 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3099 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3102 count
= len(episodes
)
3103 title
= N_('Delete %d episode?', 'Delete %d episodes?', count
) % count
3104 message
= _('Deleting episodes removes downloaded files.')
3106 if gpodder
.ui
.fremantle
:
3107 message
= '\n'.join([title
, message
])
3109 if confirm
and not self
.show_confirmation(message
, title
):
3112 progress
= ProgressIndicator(_('Deleting episodes'), \
3113 _('Please wait while episodes are deleted'), \
3114 parent
=self
.get_dialog_parent())
3116 def finish_deletion(episode_urls
, channel_urls
):
3117 progress
.on_finished()
3119 # Episodes have been deleted - persist the database
3122 self
.update_episode_list_icons(episode_urls
)
3123 self
.update_podcast_list_model(channel_urls
)
3124 self
.play_or_download()
3127 episode_urls
= set()
3128 channel_urls
= set()
3130 episodes_status_update
= []
3131 for idx
, episode
in enumerate(episodes
):
3132 progress
.on_progress(float(idx
)/float(len(episodes
)))
3133 if episode
.is_locked
and skip_locked
:
3134 log('Not deleting episode (is locked): %s', episode
.title
)
3136 log('Deleting episode: %s', episode
.title
)
3137 progress
.on_message(episode
.title
)
3138 episode
.delete_from_disk()
3139 episode_urls
.add(episode
.url
)
3140 channel_urls
.add(episode
.channel
.url
)
3141 episodes_status_update
.append(episode
)
3143 # Tell the shownotes window that we have removed the episode
3144 if self
.episode_shownotes_window
is not None and \
3145 self
.episode_shownotes_window
.episode
is not None and \
3146 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3147 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3149 # Notify the web service about the status update + upload
3150 self
.mygpo_client
.on_delete(episodes_status_update
)
3151 self
.mygpo_client
.flush()
3153 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3155 threading
.Thread(target
=thread_proc
).start()
3159 def on_itemRemoveOldEpisodes_activate( self
, widget
):
3160 if gpodder
.ui
.maemo
:
3162 ('maemo_remove_markup', None, None, _('Episode')),
3166 ('title_markup', None, None, _('Episode')),
3167 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3168 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3169 ('played_prop', None, None, _('Status')),
3170 ('age_prop', 'age_int_prop', gobject
.TYPE_INT
, _('Downloaded')),
3173 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
3174 selection_buttons
= {
3175 _('Select played'): lambda episode
: episode
.is_played
,
3176 _('Select finished'): lambda episode
: episode
.is_finished(),
3177 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3180 instructions
= _('Select the episodes you want to delete:')
3184 for channel
in self
.channels
:
3185 for episode
in channel
.get_downloaded_episodes():
3186 # Disallow deletion of locked episodes that still exist
3187 if not episode
.is_locked
or not episode
.file_exists():
3188 episodes
.append(episode
)
3189 # Automatically select played and file-less episodes
3190 selected
.append(episode
.is_played
or \
3191 not episode
.file_exists())
3193 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3194 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3195 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3196 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3197 show_episode_shownotes
=self
.show_episode_shownotes
)
3199 def on_selected_episodes_status_changed(self
):
3200 # The order of the updates here is important! When "All episodes" is
3201 # selected, the update of the podcast list model depends on the episode
3202 # list selection to determine which podcasts are affected. Updating
3203 # the episode list could remove the selection if a filter is active.
3204 self
.update_podcast_list_model(selected
=True)
3205 self
.update_episode_list_icons(selected
=True)
3208 def mark_selected_episodes_new(self
):
3209 for episode
in self
.get_selected_episodes():
3211 self
.on_selected_episodes_status_changed()
3213 def mark_selected_episodes_old(self
):
3214 for episode
in self
.get_selected_episodes():
3216 self
.on_selected_episodes_status_changed()
3218 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3219 for episode
in self
.get_selected_episodes():
3221 episode
.mark(is_played
=not episode
.is_played
)
3223 episode
.mark(is_played
=new_value
)
3224 self
.on_selected_episodes_status_changed()
3226 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3227 for episode
in self
.get_selected_episodes():
3229 episode
.mark(is_locked
=not episode
.is_locked
)
3231 episode
.mark(is_locked
=new_value
)
3232 self
.on_selected_episodes_status_changed()
3234 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3235 if self
.active_channel
is None:
3238 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3239 self
.active_channel
.update_channel_lock()
3241 for episode
in self
.active_channel
.get_all_episodes():
3242 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3244 self
.update_podcast_list_model(selected
=True)
3245 self
.update_episode_list_icons(all
=True)
3247 def on_itemUpdateChannel_activate(self
, widget
=None):
3248 if self
.active_channel
is None:
3249 title
= _('No podcast selected')
3250 message
= _('Please select a podcast in the podcasts list to update.')
3251 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3254 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3255 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3256 self
.update_feed_cache()
3258 self
.update_feed_cache(channels
=[self
.active_channel
])
3260 def on_itemUpdate_activate(self
, widget
=None):
3261 # Check if we have outstanding subscribe/unsubscribe actions
3262 if self
.on_add_remove_podcasts_mygpo():
3263 log('Update cancelled (received server changes)', sender
=self
)
3267 self
.update_feed_cache()
3269 gPodderWelcome(self
.gPodder
,
3270 center_on_widget
=self
.gPodder
,
3271 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3272 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3274 def download_episode_list_paused(self
, episodes
):
3275 self
.download_episode_list(episodes
, True)
3277 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3278 enable_update
= False
3280 for episode
in episodes
:
3281 log('Downloading episode: %s', episode
.title
, sender
= self
)
3282 if not episode
.was_downloaded(and_exists
=True):
3284 for task
in self
.download_tasks_seen
:
3285 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3286 self
.download_queue_manager
.add_task(task
, force_start
)
3287 enable_update
= True
3295 task
= download
.DownloadTask(episode
, self
.config
)
3296 except Exception, e
:
3297 d
= {'episode': episode
.title
, 'message': str(e
)}
3298 message
= _('Download error while downloading %(episode)s: %(message)s')
3299 self
.show_message(message
% d
, _('Download error'), important
=True)
3300 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3304 task
.status
= task
.PAUSED
3306 self
.mygpo_client
.on_download([task
.episode
])
3307 self
.download_queue_manager
.add_task(task
, force_start
)
3309 self
.download_status_model
.register_task(task
)
3310 enable_update
= True
3313 self
.enable_download_list_update()
3315 # Flush updated episode status
3316 self
.mygpo_client
.flush()
3318 def cancel_task_list(self
, tasks
):
3323 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3324 task
.status
= task
.CANCELLED
3325 elif task
.status
== task
.PAUSED
:
3326 task
.status
= task
.CANCELLED
3327 # Call run, so the partial file gets deleted
3330 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3331 self
.play_or_download()
3333 # Update the tab title and downloads list
3334 self
.update_downloads_list()
3336 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3337 if gpodder
.ui
.maemo
:
3339 ('maemo_markup', None, None, _('Episode')),
3341 show_notification
= notification
3344 ('title_markup', None, None, _('Episode')),
3345 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3346 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3348 show_notification
= False
3350 instructions
= _('Select the episodes you want to download:')
3352 if self
.new_episodes_window
is not None:
3353 self
.new_episodes_window
.main_window
.destroy()
3354 self
.new_episodes_window
= None
3356 def download_episodes_callback(episodes
):
3357 self
.new_episodes_window
= None
3358 self
.download_episode_list(episodes
)
3360 if selected
is None:
3361 # Select all by default
3362 selected
= [True]*len(episodes
)
3364 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3365 title
=_('New episodes available'), \
3366 instructions
=instructions
, \
3367 episodes
=episodes
, \
3369 selected
=selected
, \
3370 stock_ok_button
= 'gpodder-download', \
3371 callback
=download_episodes_callback
, \
3372 remove_callback
=lambda e
: e
.mark_old(), \
3373 remove_action
=_('Mark as old'), \
3374 remove_finished
=self
.episode_new_status_changed
, \
3375 _config
=self
.config
, \
3376 show_notification
=show_notification
, \
3377 show_episode_shownotes
=self
.show_episode_shownotes
)
3379 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3380 if not self
.offer_new_episodes():
3381 self
.show_message(_('Please check for new episodes later.'), \
3382 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3384 def get_new_episodes(self
, channels
=None):
3385 if channels
is None:
3386 channels
= self
.channels
3388 for channel
in channels
:
3389 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3390 episodes
.append(episode
)
3394 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3395 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3397 def commit_changes_to_database(self
):
3398 """This will be called after the sync process is finished"""
3401 def on_cleanup_ipod_activate(self
, widget
, *args
):
3402 self
.sync_ui
.on_cleanup_device()
3404 def on_manage_device_playlist(self
, widget
):
3405 self
.sync_ui
.on_manage_device_playlist()
3407 def show_hide_tray_icon(self
):
3408 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3409 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3410 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3411 self
.tray_icon
.set_visible(False)
3413 self
.tray_icon
= None
3415 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3416 self
.tray_icon
.set_visible(self
.is_iconified())
3417 elif self
.tray_icon
:
3418 self
.tray_icon
.set_visible(True)
3420 def on_itemShowAllEpisodes_activate(self
, widget
):
3421 self
.config
.podcast_list_view_all
= widget
.get_active()
3423 def on_itemShowToolbar_activate(self
, widget
):
3424 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3426 def on_itemShowDescription_activate(self
, widget
):
3427 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3429 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3430 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3431 if self
.config
.podcast_list_hide_boring
:
3432 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3434 self
.podcast_list_model
.set_view_mode(-1)
3436 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3438 if current
== self
.item_view_podcasts_all
:
3439 self
.podcast_list_model
.set_view_mode(-1)
3440 elif current
== self
.item_view_podcasts_downloaded
:
3441 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3442 elif current
== self
.item_view_podcasts_unplayed
:
3443 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3445 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3447 def on_item_view_episodes_changed(self
, radioaction
, current
):
3448 if current
== self
.item_view_episodes_all
:
3449 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
3450 elif current
== self
.item_view_episodes_undeleted
:
3451 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
3452 elif current
== self
.item_view_episodes_downloaded
:
3453 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3454 elif current
== self
.item_view_episodes_unplayed
:
3455 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3457 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
3459 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3460 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3462 def update_item_device( self
):
3463 if not gpodder
.ui
.fremantle
:
3464 if self
.config
.device_type
!= 'none':
3465 self
.itemDevice
.set_visible(True)
3466 self
.itemDevice
.label
= self
.get_device_name()
3468 self
.itemDevice
.set_visible(False)
3470 def properties_closed( self
):
3471 self
.preferences_dialog
= None
3472 self
.show_hide_tray_icon()
3473 self
.update_item_device()
3474 if gpodder
.ui
.maemo
:
3475 selection
= self
.treeAvailable
.get_selection()
3476 if self
.config
.maemo_enable_gestures
or \
3477 self
.config
.enable_fingerscroll
:
3478 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3480 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3482 def on_itemPreferences_activate(self
, widget
, *args
):
3483 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3484 _config
=self
.config
, \
3485 callback_finished
=self
.properties_closed
, \
3486 user_apps_reader
=self
.user_apps_reader
, \
3487 parent_window
=self
.main_window
, \
3488 mygpo_client
=self
.mygpo_client
, \
3489 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3491 # Initial message to relayout window (in case it's opened in portrait mode
3492 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3494 def on_itemDependencies_activate(self
, widget
):
3495 gPodderDependencyManager(self
.gPodder
)
3497 def on_goto_mygpo(self
, widget
):
3498 self
.mygpo_client
.open_website()
3500 def on_download_subscriptions_from_mygpo(self
, action
=None):
3501 title
= _('Login to gpodder.net')
3502 message
= _('Please login to download your subscriptions.')
3503 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3504 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3508 self
.config
.mygpo_username
= username
3509 self
.config
.mygpo_password
= password
3511 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3512 custom_title
=_('Subscriptions on gpodder.net'), \
3513 add_urls_callback
=self
.add_podcast_list
, \
3514 hide_url_entry
=True)
3516 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3517 # we do not have to hardcode the URL here
3518 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3519 url
= util
.url_add_authentication(OPML_URL
, \
3520 self
.config
.mygpo_username
, \
3521 self
.config
.mygpo_password
)
3522 dir.download_opml_file(url
)
3524 def on_mygpo_settings_activate(self
, action
=None):
3525 # This dialog is only used for Maemo 4
3526 if not gpodder
.ui
.diablo
:
3529 settings
= MygPodderSettings(self
.main_window
, \
3530 config
=self
.config
, \
3531 mygpo_client
=self
.mygpo_client
, \
3532 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3534 def on_itemAddChannel_activate(self
, widget
=None):
3535 gPodderAddPodcast(self
.gPodder
, \
3536 add_urls_callback
=self
.add_podcast_list
)
3538 def on_itemEditChannel_activate(self
, widget
, *args
):
3539 if self
.active_channel
is None:
3540 title
= _('No podcast selected')
3541 message
= _('Please select a podcast in the podcasts list to edit.')
3542 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3545 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3546 gPodderChannel(self
.main_window
, \
3547 channel
=self
.active_channel
, \
3548 callback_closed
=callback_closed
, \
3549 cover_downloader
=self
.cover_downloader
)
3551 def on_itemMassUnsubscribe_activate(self
, item
=None):
3553 ('title', None, None, _('Podcast')),
3556 # We're abusing the Episode Selector for selecting Podcasts here,
3557 # but it works and looks good, so why not? -- thp
3558 gPodderEpisodeSelector(self
.main_window
, \
3559 title
=_('Remove podcasts'), \
3560 instructions
=_('Select the podcast you want to remove.'), \
3561 episodes
=self
.channels
, \
3563 size_attribute
=None, \
3564 stock_ok_button
=_('Remove'), \
3565 callback
=self
.remove_podcast_list
, \
3566 _config
=self
.config
)
3568 def remove_podcast_list(self
, channels
, confirm
=True):
3570 log('No podcasts selected for deletion', sender
=self
)
3573 if len(channels
) == 1:
3574 title
= _('Removing podcast')
3575 info
= _('Please wait while the podcast is removed')
3576 message
= _('Do you really want to remove this podcast and its episodes?')
3578 title
= _('Removing podcasts')
3579 info
= _('Please wait while the podcasts are removed')
3580 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3582 if confirm
and not self
.show_confirmation(message
, title
):
3585 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3587 def finish_deletion(select_url
):
3588 # Upload subscription list changes to the web service
3589 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3591 # Re-load the channels and select the desired new channel
3592 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3593 progress
.on_finished()
3594 self
.update_podcasts_tab()
3599 for idx
, channel
in enumerate(channels
):
3600 # Update the UI for correct status messages
3601 progress
.on_progress(float(idx
)/float(len(channels
)))
3602 progress
.on_message(channel
.title
)
3604 # Delete downloaded episodes
3605 channel
.remove_downloaded()
3607 # cancel any active downloads from this channel
3608 for episode
in channel
.get_all_episodes():
3609 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3612 if len(channels
) == 1:
3613 # get the URL of the podcast we want to select next
3614 if channel
in self
.channels
:
3615 position
= self
.channels
.index(channel
)
3619 if position
== len(self
.channels
)-1:
3620 # this is the last podcast, so select the URL
3621 # of the item before this one (i.e. the "new last")
3622 select_url
= self
.channels
[position
-1].url
3624 # there is a podcast after the deleted one, so
3625 # we simply select the one that comes after it
3626 select_url
= self
.channels
[position
+1].url
3628 # Remove the channel and clean the database entries
3630 self
.channels
.remove(channel
)
3632 # Clean up downloads and download directories
3633 self
.clean_up_downloads()
3635 self
.channel_list_changed
= True
3636 self
.save_channels_opml()
3638 # The remaining stuff is to be done in the GTK main thread
3639 util
.idle_add(finish_deletion
, select_url
)
3641 threading
.Thread(target
=thread_proc
).start()
3643 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3644 if self
.active_channel
is None:
3645 title
= _('No podcast selected')
3646 message
= _('Please select a podcast in the podcasts list to remove.')
3647 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3650 self
.remove_podcast_list([self
.active_channel
])
3652 def get_opml_filter(self
):
3653 filter = gtk
.FileFilter()
3654 filter.add_pattern('*.opml')
3655 filter.add_pattern('*.xml')
3656 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3659 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3660 if filename
is None:
3661 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3662 # FIXME: Hildonization on Fremantle
3663 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3664 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3665 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3666 elif gpodder
.ui
.diablo
:
3667 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3668 dlg
.set_filter(self
.get_opml_filter())
3669 response
= dlg
.run()
3671 if response
== gtk
.RESPONSE_OK
:
3672 filename
= dlg
.get_filename()
3675 if filename
is not None:
3676 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3677 custom_title
=_('Import podcasts from OPML file'), \
3678 add_urls_callback
=self
.add_podcast_list
, \
3679 hide_url_entry
=True)
3680 dir.download_opml_file(filename
)
3682 def on_itemExportChannels_activate(self
, widget
, *args
):
3683 if not self
.channels
:
3684 title
= _('Nothing to export')
3685 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3686 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3689 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3690 # FIXME: Hildonization on Fremantle
3691 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3692 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3693 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3694 elif gpodder
.ui
.diablo
:
3695 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3696 dlg
.set_filter(self
.get_opml_filter())
3697 response
= dlg
.run()
3698 if response
== gtk
.RESPONSE_OK
:
3699 filename
= dlg
.get_filename()
3701 exporter
= opml
.Exporter( filename
)
3702 if exporter
.write(self
.channels
):
3703 count
= len(self
.channels
)
3704 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3705 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3707 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3711 def on_itemImportChannels_activate(self
, widget
, *args
):
3712 if gpodder
.ui
.fremantle
:
3713 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3714 self
.config
.toplist_url
, \
3715 self
.config
.opml_url
, \
3716 self
.add_podcast_list
, \
3717 self
.on_itemAddChannel_activate
, \
3718 self
.on_download_subscriptions_from_mygpo
, \
3719 self
.show_text_edit_dialog
)
3721 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3722 add_urls_callback
=self
.add_podcast_list
)
3723 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3725 def on_homepage_activate(self
, widget
, *args
):
3726 util
.open_website(gpodder
.__url
__)
3728 def on_wiki_activate(self
, widget
, *args
):
3729 util
.open_website('http://gpodder.org/wiki/User_Manual')
3731 def on_bug_tracker_activate(self
, widget
, *args
):
3732 if gpodder
.ui
.maemo
:
3733 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3735 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3737 def on_item_support_activate(self
, widget
):
3738 util
.open_website('http://gpodder.org/donate')
3740 def on_itemAbout_activate(self
, widget
, *args
):
3741 if gpodder
.ui
.fremantle
:
3742 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3743 HeAboutDialog
.present(self
.main_window
,
3746 gpodder
.__version
__,
3747 _('A podcast client with focus on usability'),
3748 gpodder
.__copyright
__,
3750 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3751 'http://gpodder.org/donate')
3754 dlg
= gtk
.AboutDialog()
3755 dlg
.set_transient_for(self
.main_window
)
3756 dlg
.set_name('gPodder')
3757 dlg
.set_version(gpodder
.__version
__)
3758 dlg
.set_copyright(gpodder
.__copyright
__)
3759 dlg
.set_comments(_('A podcast client with focus on usability'))
3760 dlg
.set_website(gpodder
.__url
__)
3761 dlg
.set_translator_credits( _('translator-credits'))
3762 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3764 if gpodder
.ui
.desktop
:
3765 # For the "GUI" version, we add some more
3766 # items to the about dialog (credits and logo)
3769 'Thomas Perl <thpinfo.com>',
3772 if os
.path
.exists(gpodder
.credits_file
):
3773 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3774 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3775 app_authors
+= credits
3777 dlg
.set_authors(app_authors
)
3779 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3781 dlg
.set_logo_icon_name('gpodder')
3785 def on_wNotebook_switch_page(self
, widget
, *args
):
3787 if gpodder
.ui
.maemo
:
3788 self
.tool_downloads
.set_active(page_num
== 1)
3789 page
= self
.wNotebook
.get_nth_page(page_num
)
3790 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3791 if page_num
== 0 and self
.active_channel
is not None:
3792 self
.set_title(self
.active_channel
.title
)
3794 self
.set_title(tab_label
)
3796 self
.play_or_download()
3797 self
.menuChannels
.set_sensitive(True)
3798 self
.menuSubscriptions
.set_sensitive(True)
3799 # The message area in the downloads tab should be hidden
3800 # when the user switches away from the downloads tab
3801 if self
.message_area
is not None:
3802 self
.message_area
.hide()
3803 self
.message_area
= None
3805 self
.menuChannels
.set_sensitive(False)
3806 self
.menuSubscriptions
.set_sensitive(False)
3807 if gpodder
.ui
.desktop
:
3808 self
.toolDownload
.set_sensitive(False)
3809 self
.toolPlay
.set_sensitive(False)
3810 self
.toolTransfer
.set_sensitive(False)
3811 self
.toolCancel
.set_sensitive(False)
3813 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3814 # double-click action of the podcast list or enter
3815 self
.treeChannels
.set_cursor(path
)
3817 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3818 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3820 if model
is not None and iter is not None:
3821 old_active_channel
= self
.active_channel
3822 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3824 if self
.active_channel
== old_active_channel
:
3827 if gpodder
.ui
.maemo
:
3828 self
.set_title(self
.active_channel
.title
)
3830 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3831 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3832 self
.itemEditChannel
.set_visible(False)
3833 self
.itemRemoveChannel
.set_visible(False)
3835 self
.itemEditChannel
.set_visible(True)
3836 self
.itemRemoveChannel
.set_visible(True)
3838 self
.active_channel
= None
3839 self
.itemEditChannel
.set_visible(False)
3840 self
.itemRemoveChannel
.set_visible(False)
3842 self
.update_episode_list_model()
3844 def on_btnEditChannel_clicked(self
, widget
, *args
):
3845 self
.on_itemEditChannel_activate( widget
, args
)
3847 def get_podcast_urls_from_selected_episodes(self
):
3848 """Get a set of podcast URLs based on the selected episodes"""
3849 return set(episode
.channel
.url
for episode
in \
3850 self
.get_selected_episodes())
3852 def get_selected_episodes(self
):
3853 """Get a list of selected episodes from treeAvailable"""
3854 selection
= self
.treeAvailable
.get_selection()
3855 model
, paths
= selection
.get_selected_rows()
3857 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3860 def on_transfer_selected_episodes(self
, widget
):
3861 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3863 def on_playback_selected_episodes(self
, widget
):
3864 self
.playback_episodes(self
.get_selected_episodes())
3866 def on_shownotes_selected_episodes(self
, widget
):
3867 episodes
= self
.get_selected_episodes()
3869 episode
= episodes
.pop(0)
3870 self
.show_episode_shownotes(episode
)
3872 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3874 def on_download_selected_episodes(self
, widget
):
3875 episodes
= self
.get_selected_episodes()
3876 self
.download_episode_list(episodes
)
3877 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3878 self
.play_or_download()
3880 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3881 """Double-click/enter action handler for treeAvailable"""
3882 # We should only have one one selected as it was double clicked!
3883 e
= self
.get_selected_episodes()[0]
3885 if (self
.config
.double_click_episode_action
== 'download'):
3886 # If the episode has already been downloaded and exists then play it
3887 if e
.was_downloaded(and_exists
=True):
3888 self
.playback_episodes(self
.get_selected_episodes())
3889 # else download it if it is not already downloading
3890 elif not self
.episode_is_downloading(e
):
3891 self
.download_episode_list([e
])
3892 self
.update_episode_list_icons([e
.url
])
3893 self
.play_or_download()
3894 elif (self
.config
.double_click_episode_action
== 'stream'):
3895 # If we happen to have downloaded this episode simple play it
3896 if e
.was_downloaded(and_exists
=True):
3897 self
.playback_episodes(self
.get_selected_episodes())
3898 # else if streaming is possible stream it
3899 elif self
.streaming_possible():
3900 self
.playback_episodes(self
.get_selected_episodes())
3902 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3903 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3905 # default action is to display show notes
3906 self
.on_shownotes_selected_episodes(widget
)
3908 def show_episode_shownotes(self
, episode
):
3909 if self
.episode_shownotes_window
is None:
3910 log('First-time use of episode window --- creating', sender
=self
)
3911 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3912 _download_episode_list
=self
.download_episode_list
, \
3913 _playback_episodes
=self
.playback_episodes
, \
3914 _delete_episode_list
=self
.delete_episode_list
, \
3915 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3916 _cancel_task_list
=self
.cancel_task_list
, \
3917 _episode_is_downloading
=self
.episode_is_downloading
, \
3918 _streaming_possible
=self
.streaming_possible())
3919 self
.episode_shownotes_window
.show(episode
)
3920 if self
.episode_is_downloading(episode
):
3921 self
.update_downloads_list()
3923 def restart_auto_update_timer(self
):
3924 if self
._auto
_update
_timer
_source
_id
is not None:
3925 log('Removing existing auto update timer.', sender
=self
)
3926 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
3927 self
._auto
_update
_timer
_source
_id
= None
3929 if self
.config
.auto_update_feeds
and \
3930 self
.config
.auto_update_frequency
:
3931 interval
= 60*1000*self
.config
.auto_update_frequency
3932 log('Setting up auto update timer with interval %d.', \
3933 self
.config
.auto_update_frequency
, sender
=self
)
3934 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
3935 interval
, self
._on
_auto
_update
_timer
)
3937 def _on_auto_update_timer(self
):
3938 log('Auto update timer fired.', sender
=self
)
3939 self
.update_feed_cache(force_update
=True)
3941 # Ask web service for sub changes (if enabled)
3942 self
.mygpo_client
.flush()
3946 def on_treeDownloads_row_activated(self
, widget
, *args
):
3947 # Use the standard way of working on the treeview
3948 selection
= self
.treeDownloads
.get_selection()
3949 (model
, paths
) = selection
.get_selected_rows()
3950 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
3952 for tree_row_reference
, task
in selected_tasks
:
3953 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
3954 task
.status
= task
.PAUSED
3955 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
3956 self
.download_queue_manager
.add_task(task
)
3957 self
.enable_download_list_update()
3958 elif task
.status
== task
.DONE
:
3959 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
3961 self
.play_or_download()
3963 # Update the tab title and downloads list
3964 self
.update_downloads_list()
3966 def on_item_cancel_download_activate(self
, widget
):
3967 if self
.wNotebook
.get_current_page() == 0:
3968 selection
= self
.treeAvailable
.get_selection()
3969 (model
, paths
) = selection
.get_selected_rows()
3970 urls
= [model
.get_value(model
.get_iter(path
), \
3971 self
.episode_list_model
.C_URL
) for path
in paths
]
3972 selected_tasks
= [task
for task
in self
.download_tasks_seen \
3973 if task
.url
in urls
]
3975 selection
= self
.treeDownloads
.get_selection()
3976 (model
, paths
) = selection
.get_selected_rows()
3977 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
3978 self
.download_status_model
.C_TASK
) for path
in paths
]
3979 self
.cancel_task_list(selected_tasks
)
3981 def on_btnCancelAll_clicked(self
, widget
, *args
):
3982 self
.cancel_task_list(self
.download_tasks_seen
)
3984 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
3985 episodes
= self
.get_selected_episodes()
3986 if len(episodes
) == 1:
3987 self
.delete_episode_list(episodes
, skip_locked
=False)
3989 self
.delete_episode_list(episodes
)
3991 def on_key_press(self
, widget
, event
):
3992 # Allow tab switching with Ctrl + PgUp/PgDown
3993 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
3994 if event
.keyval
== gtk
.keysyms
.Page_Up
:
3995 self
.wNotebook
.prev_page()
3997 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
3998 self
.wNotebook
.next_page()
4001 # After this code we only handle Maemo hardware keys,
4002 # so if we are not a Maemo app, we don't do anything
4003 if not gpodder
.ui
.maemo
:
4007 if event
.keyval
== gtk
.keysyms
.F7
: #plus
4009 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
4012 if diff
!= 0 and not self
.currently_updating
:
4013 selection
= self
.treeChannels
.get_selection()
4014 (model
, iter) = selection
.get_selected()
4015 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
4016 selection
.select_path(new_path
)
4017 self
.treeChannels
.set_cursor(new_path
)
4022 def on_iconify(self
):
4024 self
.gPodder
.set_skip_taskbar_hint(True)
4025 if self
.config
.minimize_to_tray
:
4026 self
.tray_icon
.set_visible(True)
4028 self
.gPodder
.set_skip_taskbar_hint(False)
4030 def on_uniconify(self
):
4032 self
.gPodder
.set_skip_taskbar_hint(False)
4033 if self
.config
.minimize_to_tray
:
4034 self
.tray_icon
.set_visible(False)
4036 self
.gPodder
.set_skip_taskbar_hint(False)
4038 def uniconify_main_window(self
):
4039 if self
.is_iconified():
4040 # We need to hide and then show the window in WMs like Metacity
4041 # or KWin4 to move the window to the active workspace
4042 # (see http://gpodder.org/bug/1125)
4045 self
.gPodder
.present()
4047 def iconify_main_window(self
):
4048 if not self
.is_iconified():
4049 self
.gPodder
.iconify()
4051 def update_podcasts_tab(self
):
4052 if len(self
.channels
):
4053 if gpodder
.ui
.fremantle
:
4054 self
.button_refresh
.set_title(_('Check for new episodes'))
4055 self
.button_refresh
.show()
4057 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
4059 if gpodder
.ui
.fremantle
:
4060 self
.button_refresh
.hide()
4062 self
.label2
.set_text(_('Podcasts'))
4064 @dbus.service
.method(gpodder
.dbus_interface
)
4065 def show_gui_window(self
):
4066 parent
= self
.get_dialog_parent()
4069 @dbus.service
.method(gpodder
.dbus_interface
)
4070 def subscribe_to_url(self
, url
):
4071 gPodderAddPodcast(self
.gPodder
,
4072 add_urls_callback
=self
.add_podcast_list
,
4075 @dbus.service
.method(gpodder
.dbus_interface
)
4076 def mark_episode_played(self
, filename
):
4077 if filename
is None:
4080 for channel
in self
.channels
:
4081 for episode
in channel
.get_all_episodes():
4082 fn
= episode
.local_filename(create
=False, check_only
=True)
4084 episode
.mark(is_played
=True)
4086 self
.update_episode_list_icons([episode
.url
])
4087 self
.update_podcast_list_model([episode
.channel
.url
])
4093 def main(options
=None):
4094 gobject
.threads_init()
4095 gobject
.set_application_name('gPodder')
4097 if gpodder
.ui
.maemo
:
4098 # Try to enable the custom icon theme for gPodder on Maemo
4099 settings
= gtk
.settings_get_default()
4100 settings
.set_string_property('gtk-icon-theme-name', \
4101 'gpodder', __file__
)
4102 # Extend the search path for the optified icon theme (Maemo 5)
4103 icon_theme
= gtk
.icon_theme_get_default()
4104 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
4106 gtk
.window_set_default_icon_name('gpodder')
4107 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
4110 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
4111 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
4113 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
4114 except dbus
.exceptions
.DBusException
, dbe
:
4115 log('Warning: Cannot get "on the bus".', traceback
=True)
4116 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
4117 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
4118 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
4119 dlg
.set_title('gPodder')
4124 util
.make_directory(gpodder
.home
)
4125 gpodder
.load_plugins()
4127 config
= UIConfig(gpodder
.config_file
)
4129 # Load hook modules and install the hook manager globally
4130 # if modules have been found an instantiated by the manager
4131 user_hooks
= hooks
.HookManager()
4132 if user_hooks
.has_modules():
4133 gpodder
.user_hooks
= user_hooks
4135 if gpodder
.ui
.diablo
:
4136 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4137 # folder exists there (allow moving "gpodder" between SD cards or USB)
4138 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4139 if not os
.path
.exists(config
.download_dir
):
4140 log('Downloads might have been moved. Trying to locate them...')
4141 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4142 dir = os
.path
.join(basedir
, 'gpodder')
4143 if os
.path
.exists(dir):
4144 log('Downloads found in: %s', dir)
4145 config
.download_dir
= dir
4148 log('Downloads NOT FOUND in %s', dir)
4150 if config
.enable_fingerscroll
:
4151 BuilderWidget
.use_fingerscroll
= True
4152 elif gpodder
.ui
.fremantle
:
4153 config
.on_quit_ask
= False
4155 config
.mygpo_device_type
= util
.detect_device_type()
4157 gp
= gPodder(bus_name
, config
)
4160 if options
.subscribe
:
4161 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4164 # handle "subscribe to podcast" events from firefox
4165 if platform
.system() == 'Darwin':
4166 from gpodder
import gpodderosx
4167 gpodderosx
.register_handlers(gp
)
4168 # end mac OS X stuff