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', 'label2', 'labelDownloads', 'btnUpdateFeeds']
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
443 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
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():
852 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
853 return self
.treeview_available_handle_gestures(treeview
, event
)
855 return self
.treeview_available_show_context_menu(treeview
, event
)
857 def on_treeview_downloads_button_released(self
, treeview
, event
):
858 if event
.window
!= treeview
.get_bin_window():
861 return self
.treeview_downloads_show_context_menu(treeview
, event
)
863 def on_entry_search_podcasts_changed(self
, editable
):
864 if self
.hbox_search_podcasts
.get_property('visible'):
865 self
.podcast_list_model
.set_search_term(editable
.get_chars(0, -1))
867 def on_entry_search_podcasts_key_press(self
, editable
, event
):
868 if event
.keyval
== gtk
.keysyms
.Escape
:
869 self
.hide_podcast_search()
872 def hide_podcast_search(self
, *args
):
873 self
.hbox_search_podcasts
.hide()
874 self
.entry_search_podcasts
.set_text('')
875 self
.podcast_list_model
.set_search_term(None)
876 self
.treeChannels
.grab_focus()
878 def show_podcast_search(self
, input_char
):
879 self
.hbox_search_podcasts
.show()
880 self
.entry_search_podcasts
.insert_text(input_char
, -1)
881 self
.entry_search_podcasts
.grab_focus()
882 self
.entry_search_podcasts
.set_position(-1)
884 def init_podcast_list_treeview(self
):
885 # Set up podcast channel tree view widget
886 if gpodder
.ui
.fremantle
:
887 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
888 self
.item_view_podcasts_downloaded
.set_active(True)
889 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
890 self
.item_view_podcasts_unplayed
.set_active(True)
892 self
.item_view_podcasts_all
.set_active(True)
893 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
895 iconcolumn
= gtk
.TreeViewColumn('')
896 iconcell
= gtk
.CellRendererPixbuf()
897 iconcolumn
.pack_start(iconcell
, False)
898 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
899 self
.treeChannels
.append_column(iconcolumn
)
901 namecolumn
= gtk
.TreeViewColumn('')
902 namecell
= gtk
.CellRendererText()
903 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
904 namecolumn
.pack_start(namecell
, True)
905 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
907 if gpodder
.ui
.fremantle
:
908 countcell
= gtk
.CellRendererText()
909 from gpodder
.gtkui
.frmntl
import style
910 countcell
.set_property('font-desc', style
.get_font_desc('EmpSystemFont'))
911 countcell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
912 countcell
.set_property('alignment', pango
.ALIGN_RIGHT
)
913 countcell
.set_property('xalign', 1.)
914 countcell
.set_property('xpad', 5)
915 namecolumn
.pack_start(countcell
, False)
916 namecolumn
.add_attribute(countcell
, 'text', PodcastListModel
.C_DOWNLOADS
)
917 namecolumn
.add_attribute(countcell
, 'visible', PodcastListModel
.C_DOWNLOADS
)
919 iconcell
= gtk
.CellRendererPixbuf()
920 iconcell
.set_property('xalign', 1.0)
921 namecolumn
.pack_start(iconcell
, False)
922 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
923 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
925 self
.treeChannels
.append_column(namecolumn
)
927 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
929 # When no podcast is selected, clear the episode list model
930 selection
= self
.treeChannels
.get_selection()
931 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
933 # Set up type-ahead find for the podcast list
934 def on_key_press(treeview
, event
):
935 if event
.keyval
== gtk
.keysyms
.Escape
:
936 self
.hide_podcast_search()
937 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
938 self
.hide_podcast_search()
939 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
940 # Don't handle type-ahead when control is pressed (so shortcuts
941 # with the Ctrl key still work, e.g. Ctrl+A, ...)
944 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
945 if unicode_char_id
== 0:
947 input_char
= unichr(unicode_char_id
)
948 self
.show_podcast_search(input_char
)
950 self
.treeChannels
.connect('key-press-event', on_key_press
)
952 # Enable separators to the podcast list to separate special podcasts
953 # from others (this is used for the "all episodes" view)
954 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
956 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
958 def on_entry_search_episodes_changed(self
, editable
):
959 if self
.hbox_search_episodes
.get_property('visible'):
960 self
.episode_list_model
.set_search_term(editable
.get_chars(0, -1))
962 def on_entry_search_episodes_key_press(self
, editable
, event
):
963 if event
.keyval
== gtk
.keysyms
.Escape
:
964 self
.hide_episode_search()
967 def hide_episode_search(self
, *args
):
968 self
.hbox_search_episodes
.hide()
969 self
.entry_search_episodes
.set_text('')
970 self
.episode_list_model
.set_search_term(None)
971 self
.treeAvailable
.grab_focus()
973 def show_episode_search(self
, input_char
):
974 self
.hbox_search_episodes
.show()
975 self
.entry_search_episodes
.insert_text(input_char
, -1)
976 self
.entry_search_episodes
.grab_focus()
977 self
.entry_search_episodes
.set_position(-1)
979 def init_episode_list_treeview(self
):
980 # For loading the list model
981 self
.episode_list_model
= EpisodeListModel()
983 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
984 self
.item_view_episodes_undeleted
.set_active(True)
985 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
986 self
.item_view_episodes_downloaded
.set_active(True)
987 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
988 self
.item_view_episodes_unplayed
.set_active(True)
990 self
.item_view_episodes_all
.set_active(True)
992 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
994 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
996 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
998 iconcell
= gtk
.CellRendererPixbuf()
999 iconcell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1000 if gpodder
.ui
.maemo
:
1001 iconcell
.set_fixed_size(50, 50)
1003 iconcell
.set_fixed_size(40, -1)
1005 namecell
= gtk
.CellRendererText()
1006 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1007 namecolumn
= gtk
.TreeViewColumn(_('Episode'))
1008 namecolumn
.pack_start(iconcell
, False)
1009 namecolumn
.add_attribute(iconcell
, 'icon-name', EpisodeListModel
.C_STATUS_ICON
)
1010 namecolumn
.pack_start(namecell
, True)
1011 namecolumn
.add_attribute(namecell
, 'markup', EpisodeListModel
.C_DESCRIPTION
)
1012 if gpodder
.ui
.fremantle
:
1013 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_FIXED
)
1015 namecolumn
.set_sort_column_id(EpisodeListModel
.C_DESCRIPTION
)
1016 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1017 namecolumn
.set_resizable(True)
1018 namecolumn
.set_expand(True)
1020 if gpodder
.ui
.fremantle
:
1021 from gpodder
.gtkui
.frmntl
import style
1022 timecell
= gtk
.CellRendererText()
1023 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1024 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1025 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1026 timecell
.set_property('xalign', 1.)
1027 timecell
.set_property('xpad', 5)
1028 namecolumn
.pack_start(timecell
, False)
1029 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1030 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME1_VISIBLE
)
1032 # Add another cell renderer to fix a sizing issue (one renderer
1033 # only renders short text and the other one longer text to avoid
1034 # having titles of episodes unnecessarily cut off)
1035 timecell
= gtk
.CellRendererText()
1036 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1037 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1038 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1039 timecell
.set_property('xalign', 1.)
1040 timecell
.set_property('xpad', 5)
1041 namecolumn
.pack_start(timecell
, False)
1042 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1043 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME2_VISIBLE
)
1045 lockcell
= gtk
.CellRendererPixbuf()
1046 lockcell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1047 if gpodder
.ui
.fremantle
:
1048 lockcell
.set_property('icon-name', 'general_locked')
1050 lockcell
.set_property('icon-name', 'emblem-readonly')
1052 namecolumn
.pack_start(lockcell
, False)
1053 namecolumn
.add_attribute(lockcell
, 'visible', EpisodeListModel
.C_LOCKED
)
1055 sizecell
= gtk
.CellRendererText()
1056 sizecell
.set_property('xalign', 1)
1057 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
1058 sizecolumn
.set_sort_column_id(EpisodeListModel
.C_FILESIZE
)
1060 releasecell
= gtk
.CellRendererText()
1061 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
1062 releasecolumn
.set_sort_column_id(EpisodeListModel
.C_PUBLISHED
)
1064 namecolumn
.set_reorderable(True)
1065 self
.treeAvailable
.append_column(namecolumn
)
1067 if not gpodder
.ui
.maemo
:
1068 for itemcolumn
in (sizecolumn
, releasecolumn
):
1069 itemcolumn
.set_reorderable(True)
1070 self
.treeAvailable
.append_column(itemcolumn
)
1072 # Set up type-ahead find for the episode list
1073 def on_key_press(treeview
, event
):
1074 if event
.keyval
== gtk
.keysyms
.Escape
:
1075 self
.hide_episode_search()
1076 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
1077 self
.hide_episode_search()
1078 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
1079 # Don't handle type-ahead when control is pressed (so shortcuts
1080 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1083 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1084 if unicode_char_id
== 0:
1086 input_char
= unichr(unicode_char_id
)
1087 self
.show_episode_search(input_char
)
1089 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1091 if gpodder
.ui
.desktop
and not self
.config
.enable_fingerscroll
:
1092 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1093 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1094 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1095 if self
.config
.on_drag_mark_played
:
1096 for episode
in self
.get_selected_episodes():
1097 episode
.mark(is_played
=True)
1098 self
.on_selected_episodes_status_changed()
1099 uris
= ['file://'+e
.local_filename(create
=False) \
1100 for e
in self
.get_selected_episodes() \
1101 if e
.was_downloaded(and_exists
=True)]
1102 uris
.append('') # for the trailing '\r\n'
1103 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1104 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1106 selection
= self
.treeAvailable
.get_selection()
1107 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
1108 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1109 elif gpodder
.ui
.fremantle
:
1110 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1112 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1113 # Update the sensitivity of the toolbar buttons on the Desktop
1114 selection
.connect('changed', lambda s
: self
.play_or_download())
1116 if gpodder
.ui
.diablo
:
1117 # Set up the tap-and-hold context menu for podcasts
1119 menu
.append(self
.itemUpdateChannel
.create_menu_item())
1120 menu
.append(self
.itemEditChannel
.create_menu_item())
1121 menu
.append(gtk
.SeparatorMenuItem())
1122 menu
.append(self
.itemRemoveChannel
.create_menu_item())
1123 menu
.append(gtk
.SeparatorMenuItem())
1124 item
= gtk
.ImageMenuItem(_('Close this menu'))
1125 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1126 gtk
.ICON_SIZE_MENU
))
1129 menu
= self
.set_finger_friendly(menu
)
1130 self
.treeChannels
.tap_and_hold_setup(menu
)
1133 def init_download_list_treeview(self
):
1134 # enable multiple selection support
1135 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1136 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1138 # columns and renderers for "download progress" tab
1139 # First column: [ICON] Episodename
1140 column
= gtk
.TreeViewColumn(_('Episode'))
1142 cell
= gtk
.CellRendererPixbuf()
1143 if gpodder
.ui
.maemo
:
1144 cell
.set_fixed_size(50, 50)
1145 cell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1146 column
.pack_start(cell
, expand
=False)
1147 column
.add_attribute(cell
, 'icon-name', \
1148 DownloadStatusModel
.C_ICON_NAME
)
1150 cell
= gtk
.CellRendererText()
1151 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1152 column
.pack_start(cell
, expand
=True)
1153 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1154 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1155 column
.set_expand(True)
1156 self
.treeDownloads
.append_column(column
)
1158 # Second column: Progress
1159 cell
= gtk
.CellRendererProgress()
1160 cell
.set_property('yalign', .5)
1161 cell
.set_property('ypad', 6)
1162 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1163 value
=DownloadStatusModel
.C_PROGRESS
, \
1164 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1165 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1166 column
.set_expand(False)
1167 self
.treeDownloads
.append_column(column
)
1168 if gpodder
.ui
.maemo
:
1169 column
.set_property('min-width', 200)
1170 column
.set_property('max-width', 200)
1172 column
.set_property('min-width', 150)
1173 column
.set_property('max-width', 150)
1175 self
.treeDownloads
.set_model(self
.download_status_model
)
1176 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1178 def on_treeview_expose_event(self
, treeview
, event
):
1179 if event
.window
== treeview
.get_bin_window():
1180 model
= treeview
.get_model()
1181 if (model
is not None and model
.get_iter_first() is not None):
1184 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1188 ctx
= event
.window
.cairo_create()
1189 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1190 event
.area
.width
, event
.area
.height
)
1193 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1196 if role
== TreeViewHelper
.ROLE_EPISODES
:
1197 if self
.currently_updating
:
1198 text
= _('Loading episodes')
1199 elif self
.config
.episode_list_view_mode
!= \
1200 EpisodeListModel
.VIEW_ALL
:
1201 text
= _('No episodes in current view')
1203 text
= _('No episodes available')
1204 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1205 if self
.config
.episode_list_view_mode
!= \
1206 EpisodeListModel
.VIEW_ALL
and \
1207 self
.config
.podcast_list_hide_boring
and \
1208 len(self
.channels
) > 0:
1209 text
= _('No podcasts in this view')
1211 text
= _('No subscriptions')
1212 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1213 text
= _('No active downloads')
1215 raise Exception('on_treeview_expose_event: unknown role')
1217 if gpodder
.ui
.fremantle
:
1218 from gpodder
.gtkui
.frmntl
import style
1219 font_desc
= style
.get_font_desc('LargeSystemFont')
1223 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1225 if role
== TreeViewHelper
.ROLE_EPISODES
and \
1226 self
.currently_updating
:
1231 def enable_download_list_update(self
):
1232 if not self
.download_list_update_enabled
:
1233 self
.update_downloads_list()
1234 gobject
.timeout_add(1500, self
.update_downloads_list
)
1235 self
.download_list_update_enabled
= True
1237 def cleanup_downloads(self
):
1238 model
= self
.download_status_model
1240 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1241 changed_episode_urls
= set()
1242 for row_reference
, task
in all_tasks
:
1243 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1244 model
.remove(model
.get_iter(row_reference
.get_path()))
1246 # We don't "see" this task anymore - remove it;
1247 # this is needed, so update_episode_list_icons()
1248 # below gets the correct list of "seen" tasks
1249 self
.download_tasks_seen
.remove(task
)
1250 except KeyError, key_error
:
1251 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1252 changed_episode_urls
.add(task
.url
)
1253 # Tell the task that it has been removed (so it can clean up)
1254 task
.removed_from_list()
1256 # Tell the podcasts tab to update icons for our removed podcasts
1257 self
.update_episode_list_icons(changed_episode_urls
)
1259 # Tell the shownotes window that we have removed the episode
1260 if self
.episode_shownotes_window
is not None and \
1261 self
.episode_shownotes_window
.episode
is not None and \
1262 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1263 self
.episode_shownotes_window
._download
_status
_changed
(None)
1265 # Update the downloads list one more time
1266 self
.update_downloads_list(can_call_cleanup
=False)
1268 def on_tool_downloads_toggled(self
, toolbutton
):
1269 if toolbutton
.get_active():
1270 self
.wNotebook
.set_current_page(1)
1272 self
.wNotebook
.set_current_page(0)
1274 def add_download_task_monitor(self
, monitor
):
1275 self
.download_task_monitors
.add(monitor
)
1276 model
= self
.download_status_model
1280 task
= row
[self
.download_status_model
.C_TASK
]
1281 monitor
.task_updated(task
)
1283 def remove_download_task_monitor(self
, monitor
):
1284 self
.download_task_monitors
.remove(monitor
)
1286 def update_downloads_list(self
, can_call_cleanup
=True):
1288 model
= self
.download_status_model
1290 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1291 total_speed
, total_size
, done_size
= 0, 0, 0
1293 # Keep a list of all download tasks that we've seen
1294 download_tasks_seen
= set()
1296 # Remember the DownloadTask object for the episode that
1297 # has been opened in the episode shownotes dialog (if any)
1298 if self
.episode_shownotes_window
is not None:
1299 shownotes_episode
= self
.episode_shownotes_window
.episode
1300 shownotes_task
= None
1302 shownotes_episode
= None
1303 shownotes_task
= None
1305 # Do not go through the list of the model is not (yet) available
1309 failed_downloads
= []
1311 self
.download_status_model
.request_update(row
.iter)
1313 task
= row
[self
.download_status_model
.C_TASK
]
1314 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1316 # Let the download task monitors know of changes
1317 for monitor
in self
.download_task_monitors
:
1318 monitor
.task_updated(task
)
1321 done_size
+= size
*progress
1323 if shownotes_episode
is not None and \
1324 shownotes_episode
.url
== task
.episode
.url
:
1325 shownotes_task
= task
1327 download_tasks_seen
.add(task
)
1329 if status
== download
.DownloadTask
.DOWNLOADING
:
1331 total_speed
+= speed
1332 elif status
== download
.DownloadTask
.FAILED
:
1333 failed_downloads
.append(task
)
1335 elif status
== download
.DownloadTask
.DONE
:
1337 elif status
== download
.DownloadTask
.QUEUED
:
1339 elif status
== download
.DownloadTask
.PAUSED
:
1344 # Remember which tasks we have seen after this run
1345 self
.download_tasks_seen
= download_tasks_seen
1347 if gpodder
.ui
.desktop
:
1348 text
= [_('Downloads')]
1349 if downloading
+ failed
+ queued
> 0:
1352 s
.append(N_('%d active', '%d active', downloading
) % downloading
)
1354 s
.append(N_('%d failed', '%d failed', failed
) % failed
)
1356 s
.append(N_('%d queued', '%d queued', queued
) % queued
)
1357 text
.append(' (' + ', '.join(s
)+')')
1358 self
.labelDownloads
.set_text(''.join(text
))
1359 elif gpodder
.ui
.diablo
:
1360 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1362 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1364 self
.tool_downloads
.set_label(_('Downloads'))
1365 elif gpodder
.ui
.fremantle
:
1366 if downloading
+ queued
> 0:
1367 self
.button_downloads
.set_value(N_('%d active', '%d active', downloading
+queued
) % (downloading
+queued
))
1369 self
.button_downloads
.set_value(N_('%d failed', '%d failed', failed
) % failed
)
1371 self
.button_downloads
.set_value(N_('%d paused', '%d paused', paused
) % paused
)
1373 self
.button_downloads
.set_value(_('Idle'))
1375 title
= [self
.default_title
]
1377 # We have to update all episodes/channels for which the status has
1378 # changed. Accessing task.status_changed has the side effect of
1379 # re-setting the changed flag, so we need to get the "changed" list
1380 # of tuples first and split it into two lists afterwards
1381 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1382 self
.download_tasks_seen
if task
.status_changed
]
1383 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1384 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1386 count
= downloading
+ queued
1388 title
.append(N_('downloading %d file', 'downloading %d files', count
) % count
)
1391 percentage
= 100.0*done_size
/total_size
1394 total_speed
= util
.format_filesize(total_speed
)
1395 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1396 if self
.tray_icon
is not None:
1397 # Update the tray icon status and progress bar
1398 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1399 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1401 if self
.tray_icon
is not None:
1402 # Update the tray icon status
1403 self
.tray_icon
.set_status()
1404 if gpodder
.ui
.desktop
:
1405 self
.downloads_finished(self
.download_tasks_seen
)
1406 if gpodder
.ui
.diablo
:
1407 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1408 log('All downloads have finished.', sender
=self
)
1409 if self
.config
.cmd_all_downloads_complete
:
1410 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1412 if gpodder
.ui
.fremantle
and failed
:
1413 message
= '\n'.join(['%s: %s' % (str(task
), \
1414 task
.error_message
) for task
in failed_downloads
])
1415 self
.show_message(message
, _('Downloads failed'), important
=True)
1417 # Remove finished episodes
1418 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1419 self
.cleanup_downloads()
1421 # Stop updating the download list here
1422 self
.download_list_update_enabled
= False
1424 if not gpodder
.ui
.fremantle
:
1425 self
.gPodder
.set_title(' - '.join(title
))
1427 self
.update_episode_list_icons(episode_urls
)
1428 if self
.episode_shownotes_window
is not None:
1429 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1430 shownotes_task
!= self
.episode_shownotes_window
.task
:
1431 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1432 self
.episode_shownotes_window
._download
_status
_progress
()
1433 self
.play_or_download()
1435 self
.update_podcast_list_model(channel_urls
)
1437 return self
.download_list_update_enabled
1438 except Exception, e
:
1439 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1440 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1441 # We return False here, so the update loop won't be called again,
1442 # that's why we require the restart of gPodder in the message.
1445 def on_config_changed(self
, *args
):
1446 util
.idle_add(self
._on
_config
_changed
, *args
)
1448 def _on_config_changed(self
, name
, old_value
, new_value
):
1449 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1450 self
.toolbar
.set_property('visible', new_value
)
1451 elif name
== 'videoplayer':
1452 self
.config
.video_played_dbus
= False
1453 elif name
== 'player':
1454 self
.config
.audio_played_dbus
= False
1455 elif name
== 'episode_list_descriptions':
1456 self
.update_episode_list_model()
1457 elif name
== 'episode_list_thumbnails':
1458 self
.update_episode_list_icons(all
=True)
1459 elif name
== 'rotation_mode':
1460 self
._fremantle
_rotation
.set_mode(new_value
)
1461 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1462 self
.restart_auto_update_timer()
1463 elif name
== 'podcast_list_view_all':
1464 # Force a update of the podcast list model
1465 self
.channel_list_changed
= True
1466 if gpodder
.ui
.fremantle
:
1467 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1468 while gtk
.events_pending():
1469 gtk
.main_iteration(False)
1470 self
.update_podcast_list_model()
1471 if gpodder
.ui
.fremantle
:
1472 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1474 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1475 # With get_bin_window, we get the window that contains the rows without
1476 # the header. The Y coordinate of this window will be the height of the
1477 # treeview header. This is the amount we have to subtract from the
1478 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1479 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1482 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1484 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
1485 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1488 if path
is not None:
1489 model
= treeview
.get_model()
1490 iter = model
.get_iter(path
)
1491 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1493 if role
== TreeViewHelper
.ROLE_EPISODES
:
1494 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1495 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1496 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1498 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1499 if last_tooltip
is not None and last_tooltip
!= id:
1500 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1502 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1504 if role
== TreeViewHelper
.ROLE_EPISODES
:
1505 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1507 tooltip
.set_text(description
)
1510 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1511 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1514 channel
.request_save_dir_size()
1515 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1516 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1518 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1519 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1520 table
= gtk
.Table(rows
=3, columns
=3)
1521 table
.set_row_spacings(5)
1522 table
.set_col_spacings(5)
1523 table
.set_border_width(5)
1525 heading
= gtk
.Label()
1526 heading
.set_alignment(0, 1)
1527 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1528 table
.attach(heading
, 0, 1, 0, 1)
1529 size_info
= gtk
.Label()
1530 size_info
.set_alignment(1, 1)
1531 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1532 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1533 table
.attach(size_info
, 2, 3, 0, 1)
1535 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1537 if len(channel
.description
) < 500:
1538 description
= channel
.description
1540 pos
= channel
.description
.find('\n\n')
1541 if pos
== -1 or pos
> 500:
1542 description
= channel
.description
[:498]+'[...]'
1544 description
= channel
.description
[:pos
]
1546 description
= gtk
.Label(description
)
1548 description
.set_markup(error_str
)
1549 description
.set_alignment(0, 0)
1550 description
.set_line_wrap(True)
1551 table
.attach(description
, 0, 3, 2, 3)
1554 tooltip
.set_custom(table
)
1558 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1561 def treeview_allow_tooltips(self
, treeview
, allow
):
1562 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1564 def update_m3u_playlist_clicked(self
, widget
):
1565 if self
.active_channel
is not None:
1566 self
.active_channel
.update_m3u_playlist()
1567 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1569 def treeview_handle_context_menu_click(self
, treeview
, event
):
1570 x
, y
= int(event
.x
), int(event
.y
)
1571 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1573 selection
= treeview
.get_selection()
1574 model
, paths
= selection
.get_selected_rows()
1576 if path
is None or (path
not in paths
and \
1577 event
.button
== self
.context_menu_mouse_button
):
1578 # We have right-clicked, but not into the selection,
1579 # assume we don't want to operate on the selection
1582 if path
is not None and not paths
and \
1583 event
.button
== self
.context_menu_mouse_button
:
1584 # No selection or clicked outside selection;
1585 # select the single item where we clicked
1586 treeview
.grab_focus()
1587 treeview
.set_cursor(path
, column
, 0)
1591 # Unselect any remaining items (clicked elsewhere)
1592 if hasattr(treeview
, 'is_rubber_banding_active'):
1593 if not treeview
.is_rubber_banding_active():
1594 selection
.unselect_all()
1596 selection
.unselect_all()
1600 def downloads_list_get_selection(self
, model
=None, paths
=None):
1601 if model
is None and paths
is None:
1602 selection
= self
.treeDownloads
.get_selection()
1603 model
, paths
= selection
.get_selected_rows()
1605 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1606 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1607 model
.get_value(model
.get_iter(path
), \
1608 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1610 for row_reference
, task
in selected_tasks
:
1611 if task
.status
!= download
.DownloadTask
.QUEUED
:
1613 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1614 download
.DownloadTask
.FAILED
, \
1615 download
.DownloadTask
.CANCELLED
):
1617 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1618 download
.DownloadTask
.QUEUED
, \
1619 download
.DownloadTask
.DOWNLOADING
, \
1620 download
.DownloadTask
.FAILED
):
1622 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1623 download
.DownloadTask
.DOWNLOADING
):
1625 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1626 download
.DownloadTask
.FAILED
, \
1627 download
.DownloadTask
.DONE
):
1630 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1632 def downloads_finished(self
, download_tasks_seen
):
1633 # FIXME: Filter all tasks that have already been reported
1634 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1635 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1637 if finished_downloads
and failed_downloads
:
1638 message
= self
.format_episode_list(finished_downloads
, 5)
1639 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1640 message
+= self
.format_episode_list(failed_downloads
, 5)
1641 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1642 elif finished_downloads
:
1643 message
= self
.format_episode_list(finished_downloads
)
1644 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1645 elif failed_downloads
:
1646 message
= self
.format_episode_list(failed_downloads
)
1647 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1649 # Open torrent files right after download (bug 1029)
1650 if self
.config
.open_torrent_after_download
:
1651 for task
in download_tasks_seen
:
1652 if task
.status
!= task
.DONE
:
1655 episode
= task
.episode
1656 if episode
.mimetype
!= 'application/x-bittorrent':
1659 self
.playback_episodes([episode
])
1662 def format_episode_list(self
, episode_list
, max_episodes
=10):
1664 Format a list of episode names for notifications
1666 Will truncate long episode names and limit the amount of
1667 episodes displayed (max_episodes=10).
1669 The episode_list parameter should be a list of strings.
1671 MAX_TITLE_LENGTH
= 100
1674 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1675 if len(title
) > MAX_TITLE_LENGTH
:
1676 middle
= (MAX_TITLE_LENGTH
/2)-2
1677 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1678 result
.append(saxutils
.escape(title
))
1681 more_episodes
= len(episode_list
) - max_episodes
1682 if more_episodes
> 0:
1683 result
.append('(...')
1684 result
.append(N_('%d more episode', '%d more episodes', more_episodes
) % more_episodes
)
1685 result
.append('...)')
1687 return (''.join(result
)).strip()
1689 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1690 episode_urls
= set()
1691 model
= self
.treeDownloads
.get_model()
1692 for row_reference
, task
in tasks
:
1693 if status
== download
.DownloadTask
.QUEUED
:
1694 # Only queue task when its paused/failed/cancelled (or forced)
1695 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1696 self
.download_queue_manager
.add_task(task
, force_start
)
1697 self
.enable_download_list_update()
1698 elif status
== download
.DownloadTask
.CANCELLED
:
1699 # Cancelling a download allowed when downloading/queued
1700 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1701 task
.status
= status
1702 # Cancelling paused/failed downloads requires a call to .run()
1703 elif task
.status
in (task
.PAUSED
, task
.FAILED
):
1704 task
.status
= status
1705 # Call run, so the partial file gets deleted
1707 elif status
== download
.DownloadTask
.PAUSED
:
1708 # Pausing a download only when queued/downloading
1709 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1710 task
.status
= status
1711 elif status
is None:
1712 # Remove the selected task - cancel downloading/queued tasks
1713 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1714 task
.status
= task
.CANCELLED
1715 model
.remove(model
.get_iter(row_reference
.get_path()))
1716 # Remember the URL, so we can tell the UI to update
1718 # We don't "see" this task anymore - remove it;
1719 # this is needed, so update_episode_list_icons()
1720 # below gets the correct list of "seen" tasks
1721 self
.download_tasks_seen
.remove(task
)
1722 except KeyError, key_error
:
1723 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1724 episode_urls
.add(task
.url
)
1725 # Tell the task that it has been removed (so it can clean up)
1726 task
.removed_from_list()
1728 # We can (hopefully) simply set the task status here
1729 task
.status
= status
1730 # Tell the podcasts tab to update icons for our removed podcasts
1731 self
.update_episode_list_icons(episode_urls
)
1732 # Update the tab title and downloads list
1733 self
.update_downloads_list()
1735 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1736 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1738 if not hasattr(treeview
, 'is_rubber_banding_active'):
1741 return not treeview
.is_rubber_banding_active()
1743 if event
.button
== self
.context_menu_mouse_button
:
1744 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1745 self
.downloads_list_get_selection(model
, paths
)
1747 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1748 # This creates a menu item for selection-wide actions
1749 item
= gtk
.ImageMenuItem(label
)
1750 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1751 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1752 item
.set_sensitive(sensitive
)
1753 return self
.set_finger_friendly(item
)
1757 item
= gtk
.ImageMenuItem(_('Episode details'))
1758 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1759 if len(selected_tasks
) == 1:
1760 row_reference
, task
= selected_tasks
[0]
1761 episode
= task
.episode
1762 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1764 item
.set_sensitive(False)
1765 menu
.append(self
.set_finger_friendly(item
))
1766 menu
.append(gtk
.SeparatorMenuItem())
1768 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1770 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1771 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1772 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1773 menu
.append(gtk
.SeparatorMenuItem())
1774 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1776 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
1777 # Because we open the popup on left-click for Maemo,
1778 # we also include a non-action to close the menu
1779 menu
.append(gtk
.SeparatorMenuItem())
1780 item
= gtk
.ImageMenuItem(_('Close this menu'))
1781 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1783 menu
.append(self
.set_finger_friendly(item
))
1786 menu
.popup(None, None, None, event
.button
, event
.time
)
1789 def treeview_channels_show_context_menu(self
, treeview
, event
):
1790 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1794 # Check for valid channel id, if there's no id then
1795 # assume that it is a proxy channel or equivalent
1796 # and cannot be operated with right click
1797 if self
.active_channel
.id is None:
1800 if event
.button
== 3:
1805 item
= gtk
.ImageMenuItem( _('Update podcast'))
1806 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1807 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1808 item
.set_sensitive(not self
.updating_feed_cache
)
1811 menu
.append(gtk
.SeparatorMenuItem())
1813 item
= gtk
.CheckMenuItem(_('Keep episodes'))
1814 item
.set_active(self
.active_channel
.channel_is_locked
)
1815 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1816 menu
.append(self
.set_finger_friendly(item
))
1818 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1819 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1820 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1823 if self
.config
.device_type
!= 'none':
1824 item
= gtk
.MenuItem(_('Synchronize to device'))
1825 item
.connect('activate', lambda item
: self
.on_sync_to_ipod_activate(item
, self
.active_channel
.get_downloaded_episodes()))
1828 menu
.append( gtk
.SeparatorMenuItem())
1830 item
= gtk
.ImageMenuItem(_('Podcast details'))
1831 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1832 item
.connect('activate', self
.on_itemEditChannel_activate
)
1836 # Disable tooltips while we are showing the menu, so
1837 # the tooltip will not appear over the menu
1838 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1839 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1840 menu
.popup( None, None, None, event
.button
, event
.time
)
1844 def on_itemClose_activate(self
, widget
):
1845 if self
.tray_icon
is not None:
1846 self
.iconify_main_window()
1848 self
.on_gPodder_delete_event(widget
)
1850 def cover_file_removed(self
, channel_url
):
1852 The Cover Downloader calls this when a previously-
1853 available cover has been removed from the disk. We
1854 have to update our model to reflect this change.
1856 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1858 def cover_download_finished(self
, channel
, pixbuf
):
1860 The Cover Downloader calls this when it has finished
1861 downloading (or registering, if already downloaded)
1862 a new channel cover, which is ready for displaying.
1864 self
.podcast_list_model
.add_cover_by_channel(channel
, pixbuf
)
1866 def save_episodes_as_file(self
, episodes
):
1867 for episode
in episodes
:
1868 self
.save_episode_as_file(episode
)
1870 def save_episode_as_file(self
, episode
):
1871 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1872 if episode
.was_downloaded(and_exists
=True):
1873 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1874 copy_from
= episode
.local_filename(create
=False)
1875 assert copy_from
is not None
1876 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1877 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1878 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1880 def copy_episodes_bluetooth(self
, episodes
):
1881 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1883 if gpodder
.ui
.maemo
:
1884 util
.bluetooth_send_files_maemo([e
.local_filename(create
=False) \
1885 for e
in episodes_to_copy
])
1888 def convert_and_send_thread(episode
):
1889 for episode
in episodes
:
1890 filename
= episode
.local_filename(create
=False)
1891 assert filename
is not None
1892 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1893 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1894 (base
, ext
) = os
.path
.splitext(filename
)
1895 if not destfile
.endswith(ext
):
1899 shutil
.copyfile(filename
, destfile
)
1900 util
.bluetooth_send_file(destfile
)
1902 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1903 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1905 util
.delete_file(destfile
)
1907 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1909 def get_device_name(self
):
1910 if self
.config
.device_type
== 'ipod':
1912 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1913 return _('MP3 player')
1915 return '(unknown device)'
1917 def _treeview_button_released(self
, treeview
, event
):
1918 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1919 dy
= int(abs(event
.y
-ypos
))
1920 dx
= int(event
.x
-xpos
)
1922 selection
= treeview
.get_selection()
1923 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1924 if path
is None or dy
> 30:
1925 return (False, dx
, dy
)
1927 path
, column
, x
, y
= path
1928 selection
.select_path(path
)
1929 treeview
.set_cursor(path
)
1930 treeview
.grab_focus()
1932 return (True, dx
, dy
)
1934 def treeview_channels_handle_gestures(self
, treeview
, event
):
1935 if self
.currently_updating
:
1938 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1941 if self
.config
.maemo_enable_gestures
:
1943 self
.on_itemUpdateChannel_activate()
1945 self
.on_itemEditChannel_activate(treeview
)
1949 def treeview_available_handle_gestures(self
, treeview
, event
):
1950 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1953 if self
.config
.maemo_enable_gestures
:
1955 self
.on_playback_selected_episodes(None)
1958 self
.on_shownotes_selected_episodes(None)
1961 # Pass the event to the context menu handler for treeAvailable
1962 self
.treeview_available_show_context_menu(treeview
, event
)
1966 def treeview_available_show_context_menu(self
, treeview
, event
):
1967 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1969 if not hasattr(treeview
, 'is_rubber_banding_active'):
1972 return not treeview
.is_rubber_banding_active()
1974 if event
.button
== self
.context_menu_mouse_button
:
1975 episodes
= self
.get_selected_episodes()
1976 any_locked
= any(e
.is_locked
for e
in episodes
)
1977 any_played
= any(e
.is_played
for e
in episodes
)
1978 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1979 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1980 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
1984 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1986 if open_instead_of_play
:
1987 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1989 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1991 item
= gtk
.ImageMenuItem(_('Stream'))
1992 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
1994 item
.set_sensitive(can_play
and not downloading
)
1995 item
.connect('activate', self
.on_playback_selected_episodes
)
1996 menu
.append(self
.set_finger_friendly(item
))
1999 item
= gtk
.ImageMenuItem(_('Download'))
2000 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
2001 item
.set_sensitive(can_download
)
2002 item
.connect('activate', self
.on_download_selected_episodes
)
2003 menu
.append(self
.set_finger_friendly(item
))
2005 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
2006 item
.connect('activate', self
.on_item_cancel_download_activate
)
2007 menu
.append(self
.set_finger_friendly(item
))
2009 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
2010 item
.set_sensitive(can_delete
)
2011 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
2012 menu
.append(self
.set_finger_friendly(item
))
2016 # Ok, this probably makes sense to only display for downloaded files
2018 menu
.append(gtk
.SeparatorMenuItem())
2019 share_item
= gtk
.MenuItem(_('Send to'))
2020 menu
.append(self
.set_finger_friendly(share_item
))
2021 share_menu
= gtk
.Menu()
2023 item
= gtk
.ImageMenuItem(_('Local folder'))
2024 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
2025 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
2026 share_menu
.append(self
.set_finger_friendly(item
))
2027 if self
.bluetooth_available
:
2028 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
2029 if gpodder
.ui
.maemo
:
2030 icon_name
= ICON('qgn_list_filesys_bluetooth')
2032 icon_name
= ICON('bluetooth')
2033 item
.set_image(gtk
.image_new_from_icon_name(icon_name
, gtk
.ICON_SIZE_MENU
))
2034 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
2035 share_menu
.append(self
.set_finger_friendly(item
))
2037 item
= gtk
.ImageMenuItem(self
.get_device_name())
2038 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
2039 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
2040 share_menu
.append(self
.set_finger_friendly(item
))
2042 share_item
.set_submenu(share_menu
)
2044 if (downloaded
or one_is_new
or can_download
) and not downloading
:
2045 menu
.append(gtk
.SeparatorMenuItem())
2047 item
= gtk
.CheckMenuItem(_('New'))
2048 item
.set_active(True)
2049 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
2050 menu
.append(self
.set_finger_friendly(item
))
2052 item
= gtk
.CheckMenuItem(_('New'))
2053 item
.set_active(False)
2054 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
2055 menu
.append(self
.set_finger_friendly(item
))
2058 item
= gtk
.CheckMenuItem(_('Played'))
2059 item
.set_active(any_played
)
2060 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
2061 menu
.append(self
.set_finger_friendly(item
))
2063 item
= gtk
.CheckMenuItem(_('Keep episode'))
2064 item
.set_active(any_locked
)
2065 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
2066 menu
.append(self
.set_finger_friendly(item
))
2068 menu
.append(gtk
.SeparatorMenuItem())
2069 # Single item, add episode information menu item
2070 item
= gtk
.ImageMenuItem(_('Episode details'))
2071 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
2072 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
2073 menu
.append(self
.set_finger_friendly(item
))
2075 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
2076 # Because we open the popup on left-click for Maemo,
2077 # we also include a non-action to close the menu
2078 menu
.append(gtk
.SeparatorMenuItem())
2079 item
= gtk
.ImageMenuItem(_('Close this menu'))
2080 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
2081 menu
.append(self
.set_finger_friendly(item
))
2084 # Disable tooltips while we are showing the menu, so
2085 # the tooltip will not appear over the menu
2086 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
2087 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2088 menu
.popup( None, None, None, event
.button
, event
.time
)
2092 def set_title(self
, new_title
):
2093 if not gpodder
.ui
.fremantle
:
2094 self
.default_title
= new_title
2095 self
.gPodder
.set_title(new_title
)
2097 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2099 Updates the status icons in the episode list.
2101 If urls is given, it should be a list of URLs
2102 of episodes that should be updated.
2104 If urls is None, set ONE OF selected, all to
2105 True (the former updates just the selected
2106 episodes and the latter updates all episodes).
2108 additional_args
= (self
.episode_is_downloading
, \
2109 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2110 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2112 if urls
is not None:
2113 # We have a list of URLs to walk through
2114 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2115 elif selected
and not all
:
2116 # We should update all selected episodes
2117 selection
= self
.treeAvailable
.get_selection()
2118 model
, paths
= selection
.get_selected_rows()
2119 for path
in reversed(paths
):
2120 iter = model
.get_iter(path
)
2121 self
.episode_list_model
.update_by_filter_iter(iter, \
2123 elif all
and not selected
:
2124 # We update all (even the filter-hidden) episodes
2125 self
.episode_list_model
.update_all(*additional_args
)
2127 # Wrong/invalid call - have to specify at least one parameter
2128 raise ValueError('Invalid call to update_episode_list_icons')
2130 def episode_list_status_changed(self
, episodes
):
2131 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2132 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2135 def clean_up_downloads(self
, delete_partial
=False):
2136 # Clean up temporary files left behind by old gPodder versions
2137 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2140 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2142 for tempfile
in temporary_files
:
2143 util
.delete_file(tempfile
)
2145 # Clean up empty download folders and abandoned download folders
2146 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2147 for ddir
in download_dirs
:
2148 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2149 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2150 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2151 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2152 shutil
.rmtree(ddir
, ignore_errors
=True)
2154 def streaming_possible(self
):
2155 if gpodder
.ui
.desktop
:
2156 # User has to have a media player set on the Desktop, or else we
2157 # would probably open the browser when giving a URL to xdg-open..
2158 return (self
.config
.player
and self
.config
.player
!= 'default')
2159 elif gpodder
.ui
.maemo
:
2160 # On Maemo, the default is to use the Nokia Media Player, which is
2161 # already able to deal with HTTP URLs the right way, so we
2162 # unconditionally enable streaming always on Maemo
2167 def playback_episodes_for_real(self
, episodes
):
2168 groups
= collections
.defaultdict(list)
2169 for episode
in episodes
:
2170 file_type
= episode
.file_type()
2171 if file_type
== 'video' and self
.config
.videoplayer
and \
2172 self
.config
.videoplayer
!= 'default':
2173 player
= self
.config
.videoplayer
2174 if gpodder
.ui
.diablo
:
2175 # Use the wrapper script if it's installed to crop 3GP YouTube
2176 # videos to fit the screen (looks much nicer than w/ black border)
2177 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2178 player
= 'gpodder-mplayer'
2179 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2180 player
= 'mplayer -fs %F'
2181 elif file_type
== 'audio' and self
.config
.player
and \
2182 self
.config
.player
!= 'default':
2183 player
= self
.config
.player
2187 if file_type
not in ('audio', 'video') or \
2188 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2189 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2190 # Mark episode as played in the database
2191 episode
.mark(is_played
=True)
2192 self
.mygpo_client
.on_playback([episode
])
2194 filename
= episode
.local_filename(create
=False)
2195 if filename
is None or not os
.path
.exists(filename
):
2196 filename
= episode
.url
2197 if youtube
.is_video_link(filename
):
2198 fmt_id
= self
.config
.youtube_preferred_fmt_id
2199 if gpodder
.ui
.fremantle
:
2201 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2203 # Determine the playback resume position - if the file
2204 # was played 100%, we simply start from the beginning
2205 resume_position
= episode
.current_position
2206 if resume_position
== episode
.total_time
:
2209 if gpodder
.ui
.fremantle
:
2210 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2212 # If Panucci is configured, use D-Bus on Maemo to call it
2213 if player
== 'panucci':
2215 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2216 PANUCCI_PATH
= '/panucciInterface'
2217 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2218 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2219 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2221 def on_reply(*args
):
2224 def error_handler(filename
, err
):
2225 log('Exception in D-Bus call: %s', str(err
), \
2228 # Fallback: use the command line client
2229 for command
in util
.format_desktop_command('panucci', \
2231 log('Executing: %s', repr(command
), sender
=self
)
2232 subprocess
.Popen(command
)
2234 on_error
= lambda err
: error_handler(filename
, err
)
2236 # This method only exists in Panucci > 0.9 ('new Panucci')
2237 i
.playback_from(filename
, resume_position
, \
2238 reply_handler
=on_reply
, error_handler
=on_error
)
2240 continue # This file was handled by the D-Bus call
2241 except Exception, e
:
2242 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2243 elif player
== 'MediaBox' and gpodder
.ui
.maemo
:
2245 MEDIABOX_NAME
= 'de.pycage.mediabox'
2246 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2247 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2248 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2249 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2251 def on_reply(*args
):
2255 log('Exception in D-Bus call: %s', str(err
), \
2258 i
.load(filename
, '%s/x-unknown' % file_type
, \
2259 reply_handler
=on_reply
, error_handler
=on_error
)
2261 continue # This file was handled by the D-Bus call
2262 except Exception, e
:
2263 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2265 groups
[player
].append(filename
)
2267 # Open episodes with system default player
2268 if 'default' in groups
:
2269 if gpodder
.ui
.maemo
and len(groups
['default']) > 1:
2270 # The Nokia Media Player app does not support receiving multiple
2271 # file names via D-Bus, so we simply place all file names into a
2272 # temporary M3U playlist and open that with the Media Player.
2273 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2274 util
.write_m3u_playlist(m3u_filename
, groups
['default'], extm3u
=False)
2275 util
.gui_open(m3u_filename
)
2277 for filename
in groups
['default']:
2278 log('Opening with system default: %s', filename
, sender
=self
)
2279 util
.gui_open(filename
)
2280 del groups
['default']
2281 elif gpodder
.ui
.maemo
and groups
:
2282 # When on Maemo and not opening with default, show a notification
2283 # (no startup notification for Panucci / MPlayer yet...)
2284 if len(episodes
) == 1:
2285 text
= _('Opening %s') % episodes
[0].title
2287 count
= len(episodes
)
2288 text
= N_('Opening %d episode', 'Opening %d episodes', count
) % count
2290 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2292 def destroy_banner_later(banner
):
2295 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2297 # For each type now, go and create play commands
2298 for group
in groups
:
2299 for command
in util
.format_desktop_command(group
, groups
[group
]):
2300 log('Executing: %s', repr(command
), sender
=self
)
2301 subprocess
.Popen(command
)
2303 # Persist episode status changes to the database
2306 # Flush updated episode status
2307 self
.mygpo_client
.flush()
2309 def playback_episodes(self
, episodes
):
2310 # We need to create a list, because we run through it more than once
2311 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2312 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2315 self
.playback_episodes_for_real(episodes
)
2316 except Exception, e
:
2317 log('Error in playback!', sender
=self
, traceback
=True)
2318 if gpodder
.ui
.desktop
:
2319 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2320 _('Error opening player'), widget
=self
.toolPreferences
)
2322 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2324 channel_urls
= set()
2325 episode_urls
= set()
2326 for episode
in episodes
:
2327 channel_urls
.add(episode
.channel
.url
)
2328 episode_urls
.add(episode
.url
)
2329 self
.update_episode_list_icons(episode_urls
)
2330 self
.update_podcast_list_model(channel_urls
)
2332 def play_or_download(self
):
2333 if not gpodder
.ui
.fremantle
:
2334 if self
.wNotebook
.get_current_page() > 0:
2335 if gpodder
.ui
.desktop
:
2336 self
.toolCancel
.set_sensitive(True)
2339 if self
.currently_updating
:
2340 return (False, False, False, False, False, False)
2342 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2343 ( is_played
, is_locked
) = (False,)*2
2345 open_instead_of_play
= False
2347 selection
= self
.treeAvailable
.get_selection()
2348 if selection
.count_selected_rows() > 0:
2349 (model
, paths
) = selection
.get_selected_rows()
2353 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2354 except TypeError, te
:
2355 log('Invalid episode at path %s', str(path
), sender
=self
)
2358 if episode
.file_type() not in ('audio', 'video'):
2359 open_instead_of_play
= True
2361 if episode
.was_downloaded():
2362 can_play
= episode
.was_downloaded(and_exists
=True)
2363 is_played
= episode
.is_played
2364 is_locked
= episode
.is_locked
2368 if self
.episode_is_downloading(episode
):
2373 can_download
= can_download
and not can_cancel
2374 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2375 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2376 can_delete
= not can_cancel
2378 if gpodder
.ui
.desktop
:
2379 if open_instead_of_play
:
2380 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2382 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2383 self
.toolPlay
.set_sensitive( can_play
)
2384 self
.toolDownload
.set_sensitive( can_download
)
2385 self
.toolTransfer
.set_sensitive( can_transfer
)
2386 self
.toolCancel
.set_sensitive( can_cancel
)
2388 if not gpodder
.ui
.fremantle
:
2389 self
.item_cancel_download
.set_sensitive(can_cancel
)
2390 self
.itemDownloadSelected
.set_sensitive(can_download
)
2391 self
.itemOpenSelected
.set_sensitive(can_play
)
2392 self
.itemPlaySelected
.set_sensitive(can_play
)
2393 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2394 self
.item_toggle_played
.set_sensitive(can_play
)
2395 self
.item_toggle_lock
.set_sensitive(can_play
)
2396 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2397 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2399 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2401 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2402 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2404 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2405 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2407 def episode_new_status_changed(self
, urls
):
2408 self
.update_podcast_list_model()
2409 self
.update_episode_list_icons(urls
)
2411 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2412 """Update the podcast list treeview model
2414 If urls is given, it should list the URLs of each
2415 podcast that has to be updated in the list.
2417 If selected is True, only update the model contents
2418 for the currently-selected podcast - nothing more.
2420 The caller can optionally specify "select_url",
2421 which is the URL of the podcast that is to be
2422 selected in the list after the update is complete.
2423 This only works if the podcast list has to be
2424 reloaded; i.e. something has been added or removed
2425 since the last update of the podcast list).
2427 selection
= self
.treeChannels
.get_selection()
2428 model
, iter = selection
.get_selected()
2430 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2431 # Update "all episodes" view in any case (if enabled)
2432 self
.podcast_list_model
.update_first_row()
2435 # very cheap! only update selected channel
2436 if iter is not None:
2437 # If we have selected the "all episodes" view, we have
2438 # to update all channels for selected episodes:
2439 if self
.config
.podcast_list_view_all
and \
2440 self
.podcast_list_model
.iter_is_first_row(iter):
2441 urls
= self
.get_podcast_urls_from_selected_episodes()
2442 self
.podcast_list_model
.update_by_urls(urls
)
2444 # Otherwise just update the selected row (a podcast)
2445 self
.podcast_list_model
.update_by_filter_iter(iter)
2446 elif not self
.channel_list_changed
:
2447 # we can keep the model, but have to update some
2449 # still cheaper than reloading the whole list
2450 self
.podcast_list_model
.update_all()
2452 # ok, we got a bunch of urls to update
2453 self
.podcast_list_model
.update_by_urls(urls
)
2455 if model
and iter and select_url
is None:
2456 # Get the URL of the currently-selected podcast
2457 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2459 # Update the podcast list model with new channels
2460 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2463 selected_iter
= model
.get_iter_first()
2464 # Find the previously-selected URL in the new
2465 # model if we have an URL (else select first)
2466 if select_url
is not None:
2467 pos
= model
.get_iter_first()
2468 while pos
is not None:
2469 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2470 if url
== select_url
:
2473 pos
= model
.iter_next(pos
)
2475 if not gpodder
.ui
.fremantle
:
2476 if selected_iter
is not None:
2477 selection
.select_iter(selected_iter
)
2478 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2480 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2481 self
.channel_list_changed
= False
2483 def episode_is_downloading(self
, episode
):
2484 """Returns True if the given episode is being downloaded at the moment"""
2488 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2490 def update_episode_list_model(self
):
2491 if self
.channels
and self
.active_channel
is not None:
2492 if gpodder
.ui
.fremantle
:
2493 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2495 self
.currently_updating
= True
2496 self
.treeAvailable
.hide()
2499 additional_args
= (self
.episode_is_downloading
, \
2500 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2501 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2502 self
.episode_list_model
.replace_from_channel(self
.active_channel
, *additional_args
)
2504 self
.treeAvailable
.get_selection().unselect_all()
2505 self
.treeAvailable
.show()
2506 util
.idle_add(self
.treeAvailable
.scroll_to_point
, 0, 0)
2507 self
.currently_updating
= False
2508 self
.play_or_download()
2510 if gpodder
.ui
.fremantle
:
2511 util
.idle_add(hildon
.hildon_gtk_window_set_progress_indicator
,
2512 self
.episodes_window
.main_window
, False)
2514 util
.idle_add(update
)
2516 self
.episode_list_model
.clear()
2518 @dbus.service
.method(gpodder
.dbus_interface
)
2519 def offer_new_episodes(self
, channels
=None):
2520 if gpodder
.ui
.fremantle
:
2521 # Assume that when this function is called that the
2522 # notification is not shown anymore (Maemo bug 11345)
2523 self
._fremantle
_notification
_visible
= False
2525 new_episodes
= self
.get_new_episodes(channels
)
2527 self
.new_episodes_show(new_episodes
)
2531 def add_podcast_list(self
, urls
, auth_tokens
=None):
2532 """Subscribe to a list of podcast given their URLs
2534 If auth_tokens is given, it should be a dictionary
2535 mapping URLs to (username, password) tuples."""
2537 if auth_tokens
is None:
2540 # Sort and split the URL list into five buckets
2541 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2542 for input_url
in urls
:
2543 url
= util
.normalize_feed_url(input_url
)
2545 # Fail this one because the URL is not valid
2546 failed
.append(input_url
)
2547 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2548 # A podcast already exists in the list for this URL
2549 existing
.append(url
)
2551 # This URL has survived the first round - queue for add
2553 if url
!= input_url
and input_url
in auth_tokens
:
2554 auth_tokens
[url
] = auth_tokens
[input_url
]
2559 progress
= ProgressIndicator(_('Adding podcasts'), \
2560 _('Please wait while episode information is downloaded.'), \
2561 parent
=self
.get_dialog_parent())
2563 def on_after_update():
2564 progress
.on_finished()
2565 # Report already-existing subscriptions to the user
2567 title
= _('Existing subscriptions skipped')
2568 message
= _('You are already subscribed to these podcasts:') \
2569 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2570 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2572 # Report subscriptions that require authentication
2576 title
= _('Podcast requires authentication')
2577 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2578 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2580 retry_podcasts
[url
] = auth_tokens
2582 # Stop asking the user for more login data
2585 error_messages
[url
] = _('Authentication failed')
2589 # If we have authentication data to retry, do so here
2591 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2593 # Report website redirections
2594 for url
in redirections
:
2595 title
= _('Website redirection detected')
2596 message
= _('The URL %(url)s redirects to %(target)s.') \
2597 + '\n\n' + _('Do you want to visit the website now?')
2598 message
= message
% {'url': url
, 'target': redirections
[url
]}
2599 if self
.show_confirmation(message
, title
):
2600 util
.open_website(url
)
2604 # Report failed subscriptions to the user
2606 title
= _('Could not add some podcasts')
2607 message
= _('Some podcasts could not be added to your list:') \
2608 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2609 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2610 self
.show_message(message
, title
, important
=True)
2612 # Upload subscription changes to gpodder.net
2613 self
.mygpo_client
.on_subscribe(worked
)
2615 # If at least one podcast has been added, save and update all
2616 if self
.channel_list_changed
:
2617 # Fix URLs if mygpo has rewritten them
2618 self
.rewrite_urls_mygpo()
2620 self
.save_channels_opml()
2622 # If only one podcast was added, select it after the update
2623 if len(worked
) == 1:
2628 # Update the list of subscribed podcasts
2629 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2630 self
.update_podcasts_tab()
2632 # Offer to download new episodes
2634 for podcast
in self
.channels
:
2635 if podcast
.url
in worked
:
2636 episodes
.extend(podcast
.get_all_episodes())
2639 episodes
= list(PodcastEpisode
.sort_by_pubdate(episodes
, \
2641 self
.new_episodes_show(episodes
, \
2642 selected
=[e
.check_is_new() for e
in episodes
])
2646 # After the initial sorting and splitting, try all queued podcasts
2647 length
= len(queued
)
2648 for index
, url
in enumerate(queued
):
2649 progress
.on_progress(float(index
)/float(length
))
2650 progress
.on_message(url
)
2651 log('QUEUE RUNNER: %s', url
, sender
=self
)
2653 # The URL is valid and does not exist already - subscribe!
2654 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2655 authentication_tokens
=auth_tokens
.get(url
, None), \
2656 max_episodes
=self
.config
.max_episodes_per_feed
, \
2657 download_dir
=self
.config
.download_dir
, \
2658 allow_empty_feeds
=self
.config
.allow_empty_feeds
, \
2659 mimetype_prefs
=self
.config
.mimetype_prefs
)
2662 username
, password
= util
.username_password_from_url(url
)
2663 except ValueError, ve
:
2664 username
, password
= (None, None)
2666 if username
is not None and channel
.username
is None and \
2667 password
is not None and channel
.password
is None:
2668 channel
.username
= username
2669 channel
.password
= password
2672 self
._update
_cover
(channel
)
2673 except feedcore
.AuthenticationRequired
:
2674 if url
in auth_tokens
:
2675 # Fail for wrong authentication data
2676 error_messages
[url
] = _('Authentication failed')
2679 # Queue for login dialog later
2682 except feedcore
.WifiLogin
, error
:
2683 redirections
[url
] = error
.data
2685 error_messages
[url
] = _('Redirection detected')
2687 except Exception, e
:
2688 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2689 error_messages
[url
] = str(e
)
2693 assert channel
is not None
2694 worked
.append(channel
.url
)
2695 self
.channels
.append(channel
)
2696 self
.channel_list_changed
= True
2697 util
.idle_add(on_after_update
)
2698 threading
.Thread(target
=thread_proc
).start()
2700 def save_channels_opml(self
):
2701 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2702 return exporter
.write(self
.channels
)
2704 def find_episode(self
, podcast_url
, episode_url
):
2705 """Find an episode given its podcast and episode URL
2707 The function will return a PodcastEpisode object if
2708 the episode is found, or None if it's not found.
2710 for podcast
in self
.channels
:
2711 if podcast_url
== podcast
.url
:
2712 for episode
in podcast
.get_all_episodes():
2713 if episode_url
== episode
.url
:
2718 def process_received_episode_actions(self
, updated_urls
):
2719 """Process/merge episode actions from gpodder.net
2721 This function will merge all changes received from
2722 the server to the local database and update the
2723 status of the affected episodes as necessary.
2725 indicator
= ProgressIndicator(_('Merging episode actions'), \
2726 _('Episode actions from gpodder.net are merged.'), \
2727 False, self
.get_dialog_parent())
2729 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2730 if action
.action
== 'play':
2731 episode
= self
.find_episode(action
.podcast_url
, \
2734 if episode
is not None:
2735 log('Play action for %s', episode
.url
, sender
=self
)
2736 episode
.mark(is_played
=True)
2738 if action
.timestamp
> episode
.current_position_updated
and \
2739 action
.position
is not None:
2740 log('Updating position for %s', episode
.url
, sender
=self
)
2741 episode
.current_position
= action
.position
2742 episode
.current_position_updated
= action
.timestamp
2745 log('Updating total time for %s', episode
.url
, sender
=self
)
2746 episode
.total_time
= action
.total
2749 elif action
.action
== 'delete':
2750 episode
= self
.find_episode(action
.podcast_url
, \
2753 if episode
is not None:
2754 if not episode
.was_downloaded(and_exists
=True):
2755 # Set the episode to a "deleted" state
2756 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2757 episode
.delete_from_disk()
2760 indicator
.on_message(N_('%d action processed', '%d actions processed', idx
) % idx
)
2761 gtk
.main_iteration(False)
2763 indicator
.on_finished()
2767 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2769 self
.updating_feed_cache
= False
2771 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2773 # Process received episode actions for all updated URLs
2774 self
.process_received_episode_actions(updated_urls
)
2776 self
.channel_list_changed
= True
2777 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2779 # Only search for new episodes in podcasts that have been
2780 # updated, not in other podcasts (for single-feed updates)
2781 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2783 if gpodder
.ui
.fremantle
:
2784 self
.fancy_progress_bar
.hide()
2785 self
.button_subscribe
.set_sensitive(True)
2786 self
.button_refresh
.set_sensitive(True)
2787 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2788 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2789 self
.update_podcasts_tab()
2790 self
.update_episode_list_model()
2791 if self
.feed_cache_update_cancelled
:
2794 def application_in_foreground():
2796 return any(w
.get_property('is-topmost') for w
in hildon
.WindowStack
.get_default().get_windows())
2797 except Exception, e
:
2798 log('Could not determine is-topmost', traceback
=True)
2799 # When in doubt, assume not in foreground
2803 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2804 # New episodes found, but we should do nothing
2805 self
.show_message(_('New episodes are available.'))
2806 elif self
.config
.auto_download
== 'always':
2807 count
= len(episodes
)
2808 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2809 self
.show_message(title
)
2810 self
.download_episode_list(episodes
)
2811 elif self
.config
.auto_download
== 'queue':
2812 self
.show_message(_('New episodes have been added to the download list.'))
2813 self
.download_episode_list_paused(episodes
)
2814 elif application_in_foreground():
2815 if not self
._fremantle
_notification
_visible
:
2816 self
.new_episodes_show(episodes
)
2817 elif not self
._fremantle
_notification
_visible
:
2820 pynotify
.init('gPodder')
2821 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2822 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2823 n
.set_hint('dbus-callback-default', ' '.join([
2824 gpodder
.dbus_bus_name
,
2825 gpodder
.dbus_gui_object_path
,
2826 gpodder
.dbus_interface
,
2827 'offer_new_episodes',
2829 n
.set_category('gpodder-new-episodes')
2831 self
._fremantle
_notification
_visible
= True
2832 except Exception, e
:
2833 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2834 self
.new_episodes_show(episodes
)
2835 self
._fremantle
_notification
_visible
= False
2836 elif not self
.config
.auto_update_feeds
:
2837 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2841 self
.tray_icon
.set_status()
2843 if self
.feed_cache_update_cancelled
:
2844 # The user decided to abort the feed update
2845 self
.show_update_feeds_buttons()
2847 # Nothing new here - but inform the user
2848 self
.pbFeedUpdate
.set_fraction(1.0)
2849 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2850 self
.feed_cache_update_cancelled
= True
2851 self
.btnCancelFeedUpdate
.show()
2852 self
.btnCancelFeedUpdate
.set_sensitive(True)
2853 self
.itemUpdate
.set_sensitive(True)
2854 if gpodder
.ui
.maemo
:
2855 # btnCancelFeedUpdate is a ToolButton on Maemo
2856 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2858 # btnCancelFeedUpdate is a normal gtk.Button
2859 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2861 count
= len(episodes
)
2862 # New episodes are available
2863 self
.pbFeedUpdate
.set_fraction(1.0)
2864 # Are we minimized and should we auto download?
2865 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2866 self
.download_episode_list(episodes
)
2867 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2868 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2869 self
.show_update_feeds_buttons()
2870 elif self
.config
.auto_download
== 'queue':
2871 self
.download_episode_list_paused(episodes
)
2872 title
= N_('%d new episode added to download list.', '%d new episodes added to download list.', count
) % count
2873 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2874 self
.show_update_feeds_buttons()
2876 self
.show_update_feeds_buttons()
2877 # New episodes are available and we are not minimized
2878 if not self
.config
.do_not_show_new_episodes_dialog
:
2879 self
.new_episodes_show(episodes
, notification
=True)
2881 message
= N_('%d new episode available', '%d new episodes available', count
) % count
2882 self
.pbFeedUpdate
.set_text(message
)
2884 def _update_cover(self
, channel
):
2885 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2886 self
.cover_downloader
.request_cover(channel
)
2888 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2889 total
= len(channels
)
2891 for updated
, channel
in enumerate(channels
):
2892 if not self
.feed_cache_update_cancelled
:
2894 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
, \
2895 mimetype_prefs
=self
.config
.mimetype_prefs
)
2896 self
._update
_cover
(channel
)
2897 except Exception, e
:
2898 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2900 message
= _('Error while updating %(url)s: %(message)s')
2902 message
= _('The feed at %(url)s could not be updated.')
2903 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2904 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2906 if self
.feed_cache_update_cancelled
:
2909 # By the time we get here the update may have already been cancelled
2910 if not self
.feed_cache_update_cancelled
:
2911 def update_progress():
2912 d
= {'podcast': channel
.title
, 'position': updated
+1, 'total': total
}
2913 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2914 self
.pbFeedUpdate
.set_text(progression
)
2916 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2917 self
.pbFeedUpdate
.set_fraction(float(updated
+1)/float(total
))
2918 util
.idle_add(update_progress
)
2920 updated_urls
= [c
.url
for c
in channels
]
2921 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2923 def show_update_feeds_buttons(self
):
2924 # Make sure that the buttons for updating feeds
2925 # appear - this should happen after a feed update
2926 if gpodder
.ui
.maemo
:
2927 self
.btnUpdateSelectedFeed
.show()
2928 self
.toolFeedUpdateProgress
.hide()
2929 self
.btnCancelFeedUpdate
.hide()
2930 self
.btnCancelFeedUpdate
.set_is_important(False)
2931 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2932 self
.toolbarSpacer
.set_expand(True)
2933 self
.toolbarSpacer
.set_draw(False)
2935 self
.hboxUpdateFeeds
.hide()
2936 self
.btnUpdateFeeds
.show()
2937 self
.itemUpdate
.set_sensitive(True)
2938 self
.itemUpdateChannel
.set_sensitive(True)
2940 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2941 if not self
.feed_cache_update_cancelled
:
2942 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2943 self
.feed_cache_update_cancelled
= True
2944 if not gpodder
.ui
.fremantle
:
2945 self
.btnCancelFeedUpdate
.set_sensitive(False)
2946 elif not gpodder
.ui
.fremantle
:
2947 self
.show_update_feeds_buttons()
2949 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2950 if self
.updating_feed_cache
:
2951 if gpodder
.ui
.fremantle
:
2952 self
.feed_cache_update_cancelled
= True
2955 if not force_update
:
2956 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2957 self
.channel_list_changed
= True
2958 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2961 # Fix URLs if mygpo has rewritten them
2962 self
.rewrite_urls_mygpo()
2964 self
.updating_feed_cache
= True
2966 if channels
is None:
2967 # Only update podcasts for which updates are enabled
2968 channels
= [c
for c
in self
.channels
if c
.feed_update_enabled
]
2970 if gpodder
.ui
.fremantle
:
2971 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2972 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2973 self
.fancy_progress_bar
.show()
2974 self
.button_subscribe
.set_sensitive(False)
2975 self
.button_refresh
.set_sensitive(False)
2976 self
.feed_cache_update_cancelled
= False
2978 self
.itemUpdate
.set_sensitive(False)
2979 self
.itemUpdateChannel
.set_sensitive(False)
2982 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2984 self
.feed_cache_update_cancelled
= False
2985 self
.btnCancelFeedUpdate
.show()
2986 self
.btnCancelFeedUpdate
.set_sensitive(True)
2987 if gpodder
.ui
.maemo
:
2988 self
.toolbarSpacer
.set_expand(False)
2989 self
.toolbarSpacer
.set_draw(True)
2990 self
.btnUpdateSelectedFeed
.hide()
2991 self
.toolFeedUpdateProgress
.show_all()
2993 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2994 self
.hboxUpdateFeeds
.show_all()
2995 self
.btnUpdateFeeds
.hide()
2997 if len(channels
) == 1:
2998 text
= _('Updating "%s"...') % channels
[0].title
3000 count
= len(channels
)
3001 text
= N_('Updating %d feed...', 'Updating %d feeds...', count
) % count
3002 self
.pbFeedUpdate
.set_text(text
)
3003 self
.pbFeedUpdate
.set_fraction(0)
3005 args
= (channels
, select_url_afterwards
)
3006 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
3008 def on_gPodder_delete_event(self
, widget
, *args
):
3009 """Called when the GUI wants to close the window
3010 Displays a confirmation dialog (and closes/hides gPodder)
3013 downloading
= self
.download_status_model
.are_downloads_in_progress()
3015 # Only iconify if we are using the window's "X" button,
3016 # but not when we are using "Quit" in the menu or toolbar
3017 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
3018 self
.iconify_main_window()
3019 elif self
.config
.on_quit_ask
or downloading
:
3020 if gpodder
.ui
.fremantle
:
3021 self
.close_gpodder()
3022 elif gpodder
.ui
.diablo
:
3023 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
3025 self
.close_gpodder()
3028 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
3029 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3030 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
3032 title
= _('Quit gPodder')
3034 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3036 message
= _('Do you really want to quit gPodder now?')
3038 dialog
.set_title(title
)
3039 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
3041 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
3042 dialog
.vbox
.pack_start(cb_ask
)
3045 quit_button
.grab_focus()
3046 result
= dialog
.run()
3049 if result
== gtk
.RESPONSE_CLOSE
:
3050 if not downloading
and cb_ask
.get_active() == True:
3051 self
.config
.on_quit_ask
= False
3052 self
.close_gpodder()
3054 self
.close_gpodder()
3058 def close_gpodder(self
):
3059 """ clean everything and exit properly
3062 if self
.save_channels_opml():
3063 pass # FIXME: Add mygpo synchronization here
3065 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
3069 if self
.tray_icon
is not None:
3070 self
.tray_icon
.set_visible(False)
3072 # Notify all tasks to to carry out any clean-up actions
3073 self
.download_status_model
.tell_all_tasks_to_quit()
3075 while gtk
.events_pending():
3076 gtk
.main_iteration(False)
3083 def get_expired_episodes(self
):
3084 for channel
in self
.channels
:
3085 for episode
in channel
.get_downloaded_episodes():
3086 # Never consider locked episodes as old
3087 if episode
.is_locked
:
3090 # Never consider fresh episodes as old
3091 if episode
.age_in_days() < self
.config
.episode_old_age
:
3094 # Do not delete played episodes (except if configured)
3095 if episode
.is_played
:
3096 if not self
.config
.auto_remove_played_episodes
:
3099 # Do not delete unplayed episodes (except if configured)
3100 if not episode
.is_played
:
3101 if not self
.config
.auto_remove_unplayed_episodes
:
3106 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3111 episodes
= [e
for e
in episodes
if not e
.is_locked
]
3114 title
= _('Episodes are locked')
3115 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3116 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3119 count
= len(episodes
)
3120 title
= N_('Delete %d episode?', 'Delete %d episodes?', count
) % count
3121 message
= _('Deleting episodes removes downloaded files.')
3123 if gpodder
.ui
.fremantle
:
3124 message
= '\n'.join([title
, message
])
3126 if confirm
and not self
.show_confirmation(message
, title
):
3129 progress
= ProgressIndicator(_('Deleting episodes'), \
3130 _('Please wait while episodes are deleted'), \
3131 parent
=self
.get_dialog_parent())
3133 def finish_deletion(episode_urls
, channel_urls
):
3134 progress
.on_finished()
3136 # Episodes have been deleted - persist the database
3139 self
.update_episode_list_icons(episode_urls
)
3140 self
.update_podcast_list_model(channel_urls
)
3141 self
.play_or_download()
3144 episode_urls
= set()
3145 channel_urls
= set()
3147 episodes_status_update
= []
3148 for idx
, episode
in enumerate(episodes
):
3149 progress
.on_progress(float(idx
)/float(len(episodes
)))
3150 if episode
.is_locked
and skip_locked
:
3151 log('Not deleting episode (is locked): %s', episode
.title
)
3153 log('Deleting episode: %s', episode
.title
)
3154 progress
.on_message(episode
.title
)
3155 episode
.delete_from_disk()
3156 episode_urls
.add(episode
.url
)
3157 channel_urls
.add(episode
.channel
.url
)
3158 episodes_status_update
.append(episode
)
3160 # Tell the shownotes window that we have removed the episode
3161 if self
.episode_shownotes_window
is not None and \
3162 self
.episode_shownotes_window
.episode
is not None and \
3163 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3164 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3166 # Notify the web service about the status update + upload
3167 self
.mygpo_client
.on_delete(episodes_status_update
)
3168 self
.mygpo_client
.flush()
3170 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3172 threading
.Thread(target
=thread_proc
).start()
3176 def on_itemRemoveOldEpisodes_activate( self
, widget
):
3177 if gpodder
.ui
.maemo
:
3179 ('maemo_remove_markup', None, None, _('Episode')),
3183 ('title_markup', None, None, _('Episode')),
3184 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3185 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3186 ('played_prop', None, None, _('Status')),
3187 ('age_prop', 'age_int_prop', gobject
.TYPE_INT
, _('Downloaded')),
3190 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
3191 selection_buttons
= {
3192 _('Select played'): lambda episode
: episode
.is_played
,
3193 _('Select finished'): lambda episode
: episode
.is_finished(),
3194 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3197 instructions
= _('Select the episodes you want to delete:')
3201 for channel
in self
.channels
:
3202 for episode
in channel
.get_downloaded_episodes():
3203 # Disallow deletion of locked episodes that still exist
3204 if not episode
.is_locked
or not episode
.file_exists():
3205 episodes
.append(episode
)
3206 # Automatically select played and file-less episodes
3207 selected
.append(episode
.is_played
or \
3208 not episode
.file_exists())
3210 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3211 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3212 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3213 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3214 show_episode_shownotes
=self
.show_episode_shownotes
)
3216 def on_selected_episodes_status_changed(self
):
3217 # The order of the updates here is important! When "All episodes" is
3218 # selected, the update of the podcast list model depends on the episode
3219 # list selection to determine which podcasts are affected. Updating
3220 # the episode list could remove the selection if a filter is active.
3221 self
.update_podcast_list_model(selected
=True)
3222 self
.update_episode_list_icons(selected
=True)
3225 def mark_selected_episodes_new(self
):
3226 for episode
in self
.get_selected_episodes():
3228 self
.on_selected_episodes_status_changed()
3230 def mark_selected_episodes_old(self
):
3231 for episode
in self
.get_selected_episodes():
3233 self
.on_selected_episodes_status_changed()
3235 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3236 for episode
in self
.get_selected_episodes():
3238 episode
.mark(is_played
=not episode
.is_played
)
3240 episode
.mark(is_played
=new_value
)
3241 self
.on_selected_episodes_status_changed()
3243 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3244 for episode
in self
.get_selected_episodes():
3246 episode
.mark(is_locked
=not episode
.is_locked
)
3248 episode
.mark(is_locked
=new_value
)
3249 self
.on_selected_episodes_status_changed()
3251 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3252 if self
.active_channel
is None:
3255 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3256 self
.active_channel
.update_channel_lock()
3258 for episode
in self
.active_channel
.get_all_episodes():
3259 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3261 self
.update_podcast_list_model(selected
=True)
3262 self
.update_episode_list_icons(all
=True)
3264 def on_itemUpdateChannel_activate(self
, widget
=None):
3265 if self
.active_channel
is None:
3266 title
= _('No podcast selected')
3267 message
= _('Please select a podcast in the podcasts list to update.')
3268 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3271 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3272 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3273 self
.update_feed_cache()
3275 self
.update_feed_cache(channels
=[self
.active_channel
])
3277 def on_itemUpdate_activate(self
, widget
=None):
3278 # Check if we have outstanding subscribe/unsubscribe actions
3279 if self
.on_add_remove_podcasts_mygpo():
3280 log('Update cancelled (received server changes)', sender
=self
)
3284 self
.update_feed_cache()
3286 gPodderWelcome(self
.gPodder
,
3287 center_on_widget
=self
.gPodder
,
3288 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3289 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3291 def download_episode_list_paused(self
, episodes
):
3292 self
.download_episode_list(episodes
, True)
3294 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3295 enable_update
= False
3297 for episode
in episodes
:
3298 log('Downloading episode: %s', episode
.title
, sender
= self
)
3299 if not episode
.was_downloaded(and_exists
=True):
3301 for task
in self
.download_tasks_seen
:
3302 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3303 self
.download_queue_manager
.add_task(task
, force_start
)
3304 enable_update
= True
3312 task
= download
.DownloadTask(episode
, self
.config
)
3313 except Exception, e
:
3314 d
= {'episode': episode
.title
, 'message': str(e
)}
3315 message
= _('Download error while downloading %(episode)s: %(message)s')
3316 self
.show_message(message
% d
, _('Download error'), important
=True)
3317 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3321 task
.status
= task
.PAUSED
3323 self
.mygpo_client
.on_download([task
.episode
])
3324 self
.download_queue_manager
.add_task(task
, force_start
)
3326 self
.download_status_model
.register_task(task
)
3327 enable_update
= True
3330 self
.enable_download_list_update()
3332 # Flush updated episode status
3333 self
.mygpo_client
.flush()
3335 def cancel_task_list(self
, tasks
):
3340 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3341 task
.status
= task
.CANCELLED
3342 elif task
.status
== task
.PAUSED
:
3343 task
.status
= task
.CANCELLED
3344 # Call run, so the partial file gets deleted
3347 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3348 self
.play_or_download()
3350 # Update the tab title and downloads list
3351 self
.update_downloads_list()
3353 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3354 if gpodder
.ui
.maemo
:
3356 ('maemo_markup', None, None, _('Episode')),
3358 show_notification
= notification
3361 ('title_markup', None, None, _('Episode')),
3362 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3363 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3365 show_notification
= False
3367 instructions
= _('Select the episodes you want to download:')
3369 if self
.new_episodes_window
is not None:
3370 self
.new_episodes_window
.main_window
.destroy()
3371 self
.new_episodes_window
= None
3373 def download_episodes_callback(episodes
):
3374 self
.new_episodes_window
= None
3375 self
.download_episode_list(episodes
)
3377 if selected
is None:
3378 # Select all by default
3379 selected
= [True]*len(episodes
)
3381 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3382 title
=_('New episodes available'), \
3383 instructions
=instructions
, \
3384 episodes
=episodes
, \
3386 selected
=selected
, \
3387 stock_ok_button
= 'gpodder-download', \
3388 callback
=download_episodes_callback
, \
3389 remove_callback
=lambda e
: e
.mark_old(), \
3390 remove_action
=_('Mark as old'), \
3391 remove_finished
=self
.episode_new_status_changed
, \
3392 _config
=self
.config
, \
3393 show_notification
=show_notification
, \
3394 show_episode_shownotes
=self
.show_episode_shownotes
)
3396 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3397 if not self
.offer_new_episodes():
3398 self
.show_message(_('Please check for new episodes later.'), \
3399 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3401 def get_new_episodes(self
, channels
=None):
3402 if channels
is None:
3403 channels
= self
.channels
3405 for channel
in channels
:
3406 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3407 episodes
.append(episode
)
3411 @dbus.service
.method(gpodder
.dbus_interface
)
3412 def start_device_synchronization(self
):
3413 """Public D-Bus API for starting Device sync (Desktop only)
3415 This method can be called to initiate a synchronization with
3416 a configured protable media player. This only works for the
3417 Desktop version of gPodder and does nothing on Maemo.
3419 if gpodder
.ui
.desktop
:
3420 self
.on_sync_to_ipod_activate(None)
3425 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3426 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3428 def commit_changes_to_database(self
):
3429 """This will be called after the sync process is finished"""
3432 def on_cleanup_ipod_activate(self
, widget
, *args
):
3433 self
.sync_ui
.on_cleanup_device()
3435 def on_manage_device_playlist(self
, widget
):
3436 self
.sync_ui
.on_manage_device_playlist()
3438 def show_hide_tray_icon(self
):
3439 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3440 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3441 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3442 self
.tray_icon
.set_visible(False)
3444 self
.tray_icon
= None
3446 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3447 self
.tray_icon
.set_visible(self
.is_iconified())
3448 elif self
.tray_icon
:
3449 self
.tray_icon
.set_visible(True)
3451 def on_itemShowAllEpisodes_activate(self
, widget
):
3452 self
.config
.podcast_list_view_all
= widget
.get_active()
3454 def on_itemShowToolbar_activate(self
, widget
):
3455 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3457 def on_itemShowDescription_activate(self
, widget
):
3458 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3460 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3461 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3462 if self
.config
.podcast_list_hide_boring
:
3463 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3465 self
.podcast_list_model
.set_view_mode(-1)
3467 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3469 if current
== self
.item_view_podcasts_all
:
3470 self
.podcast_list_model
.set_view_mode(-1)
3471 elif current
== self
.item_view_podcasts_downloaded
:
3472 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3473 elif current
== self
.item_view_podcasts_unplayed
:
3474 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3476 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3478 def on_item_view_episodes_changed(self
, radioaction
, current
):
3479 if current
== self
.item_view_episodes_all
:
3480 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
3481 elif current
== self
.item_view_episodes_undeleted
:
3482 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
3483 elif current
== self
.item_view_episodes_downloaded
:
3484 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3485 elif current
== self
.item_view_episodes_unplayed
:
3486 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3488 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
3490 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3491 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3493 def update_item_device( self
):
3494 if not gpodder
.ui
.fremantle
:
3495 if self
.config
.device_type
!= 'none':
3496 self
.itemDevice
.set_visible(True)
3497 self
.itemDevice
.label
= self
.get_device_name()
3499 self
.itemDevice
.set_visible(False)
3501 def properties_closed( self
):
3502 self
.preferences_dialog
= None
3503 self
.show_hide_tray_icon()
3504 self
.update_item_device()
3505 if gpodder
.ui
.maemo
:
3506 selection
= self
.treeAvailable
.get_selection()
3507 if self
.config
.maemo_enable_gestures
or \
3508 self
.config
.enable_fingerscroll
:
3509 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3511 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3513 def on_itemPreferences_activate(self
, widget
, *args
):
3514 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3515 _config
=self
.config
, \
3516 callback_finished
=self
.properties_closed
, \
3517 user_apps_reader
=self
.user_apps_reader
, \
3518 parent_window
=self
.main_window
, \
3519 mygpo_client
=self
.mygpo_client
, \
3520 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3522 # Initial message to relayout window (in case it's opened in portrait mode
3523 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3525 def on_itemDependencies_activate(self
, widget
):
3526 gPodderDependencyManager(self
.gPodder
)
3528 def on_goto_mygpo(self
, widget
):
3529 self
.mygpo_client
.open_website()
3531 def on_download_subscriptions_from_mygpo(self
, action
=None):
3532 title
= _('Login to gpodder.net')
3533 message
= _('Please login to download your subscriptions.')
3534 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3535 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3539 self
.config
.mygpo_username
= username
3540 self
.config
.mygpo_password
= password
3542 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3543 custom_title
=_('Subscriptions on gpodder.net'), \
3544 add_urls_callback
=self
.add_podcast_list
, \
3545 hide_url_entry
=True)
3547 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3548 # we do not have to hardcode the URL here
3549 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3550 url
= util
.url_add_authentication(OPML_URL
, \
3551 self
.config
.mygpo_username
, \
3552 self
.config
.mygpo_password
)
3553 dir.download_opml_file(url
)
3555 def on_mygpo_settings_activate(self
, action
=None):
3556 # This dialog is only used for Maemo 4
3557 if not gpodder
.ui
.diablo
:
3560 settings
= MygPodderSettings(self
.main_window
, \
3561 config
=self
.config
, \
3562 mygpo_client
=self
.mygpo_client
, \
3563 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3565 def on_itemAddChannel_activate(self
, widget
=None):
3566 gPodderAddPodcast(self
.gPodder
, \
3567 add_urls_callback
=self
.add_podcast_list
)
3569 def on_itemEditChannel_activate(self
, widget
, *args
):
3570 if self
.active_channel
is None:
3571 title
= _('No podcast selected')
3572 message
= _('Please select a podcast in the podcasts list to edit.')
3573 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3576 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3577 gPodderChannel(self
.main_window
, \
3578 channel
=self
.active_channel
, \
3579 callback_closed
=callback_closed
, \
3580 cover_downloader
=self
.cover_downloader
)
3582 def on_itemMassUnsubscribe_activate(self
, item
=None):
3584 ('title', None, None, _('Podcast')),
3587 # We're abusing the Episode Selector for selecting Podcasts here,
3588 # but it works and looks good, so why not? -- thp
3589 gPodderEpisodeSelector(self
.main_window
, \
3590 title
=_('Remove podcasts'), \
3591 instructions
=_('Select the podcast you want to remove.'), \
3592 episodes
=self
.channels
, \
3594 size_attribute
=None, \
3595 stock_ok_button
=_('Remove'), \
3596 callback
=self
.remove_podcast_list
, \
3597 _config
=self
.config
)
3599 def remove_podcast_list(self
, channels
, confirm
=True):
3601 log('No podcasts selected for deletion', sender
=self
)
3604 if len(channels
) == 1:
3605 title
= _('Removing podcast')
3606 info
= _('Please wait while the podcast is removed')
3607 message
= _('Do you really want to remove this podcast and its episodes?')
3609 title
= _('Removing podcasts')
3610 info
= _('Please wait while the podcasts are removed')
3611 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3613 if confirm
and not self
.show_confirmation(message
, title
):
3616 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3618 def finish_deletion(select_url
):
3619 # Upload subscription list changes to the web service
3620 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3622 # Re-load the channels and select the desired new channel
3623 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3624 progress
.on_finished()
3625 self
.update_podcasts_tab()
3630 for idx
, channel
in enumerate(channels
):
3631 # Update the UI for correct status messages
3632 progress
.on_progress(float(idx
)/float(len(channels
)))
3633 progress
.on_message(channel
.title
)
3635 # Delete downloaded episodes
3636 channel
.remove_downloaded()
3638 # cancel any active downloads from this channel
3639 for episode
in channel
.get_all_episodes():
3640 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3643 if len(channels
) == 1:
3644 # get the URL of the podcast we want to select next
3645 if channel
in self
.channels
:
3646 position
= self
.channels
.index(channel
)
3650 if position
== len(self
.channels
)-1:
3651 # this is the last podcast, so select the URL
3652 # of the item before this one (i.e. the "new last")
3653 select_url
= self
.channels
[position
-1].url
3655 # there is a podcast after the deleted one, so
3656 # we simply select the one that comes after it
3657 select_url
= self
.channels
[position
+1].url
3659 # Remove the channel and clean the database entries
3661 self
.channels
.remove(channel
)
3663 # Clean up downloads and download directories
3664 self
.clean_up_downloads()
3666 self
.channel_list_changed
= True
3667 self
.save_channels_opml()
3669 # The remaining stuff is to be done in the GTK main thread
3670 util
.idle_add(finish_deletion
, select_url
)
3672 threading
.Thread(target
=thread_proc
).start()
3674 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3675 if self
.active_channel
is None:
3676 title
= _('No podcast selected')
3677 message
= _('Please select a podcast in the podcasts list to remove.')
3678 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3681 self
.remove_podcast_list([self
.active_channel
])
3683 def get_opml_filter(self
):
3684 filter = gtk
.FileFilter()
3685 filter.add_pattern('*.opml')
3686 filter.add_pattern('*.xml')
3687 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3690 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3691 if filename
is None:
3692 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3693 # FIXME: Hildonization on Fremantle
3694 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3695 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3696 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3697 elif gpodder
.ui
.diablo
:
3698 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3699 dlg
.set_filter(self
.get_opml_filter())
3700 response
= dlg
.run()
3702 if response
== gtk
.RESPONSE_OK
:
3703 filename
= dlg
.get_filename()
3706 if filename
is not None:
3707 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3708 custom_title
=_('Import podcasts from OPML file'), \
3709 add_urls_callback
=self
.add_podcast_list
, \
3710 hide_url_entry
=True)
3711 dir.download_opml_file(filename
)
3713 def on_itemExportChannels_activate(self
, widget
, *args
):
3714 if not self
.channels
:
3715 title
= _('Nothing to export')
3716 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3717 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3720 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3721 # FIXME: Hildonization on Fremantle
3722 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3723 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3724 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3725 elif gpodder
.ui
.diablo
:
3726 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3727 dlg
.set_filter(self
.get_opml_filter())
3728 response
= dlg
.run()
3729 if response
== gtk
.RESPONSE_OK
:
3730 filename
= dlg
.get_filename()
3732 exporter
= opml
.Exporter( filename
)
3733 if exporter
.write(self
.channels
):
3734 count
= len(self
.channels
)
3735 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3736 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3738 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3742 def on_itemImportChannels_activate(self
, widget
, *args
):
3743 if gpodder
.ui
.fremantle
:
3744 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3745 self
.config
.toplist_url
, \
3746 self
.config
.opml_url
, \
3747 self
.add_podcast_list
, \
3748 self
.on_itemAddChannel_activate
, \
3749 self
.on_download_subscriptions_from_mygpo
, \
3750 self
.show_text_edit_dialog
)
3752 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3753 add_urls_callback
=self
.add_podcast_list
)
3754 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3756 def on_homepage_activate(self
, widget
, *args
):
3757 util
.open_website(gpodder
.__url
__)
3759 def on_wiki_activate(self
, widget
, *args
):
3760 util
.open_website('http://gpodder.org/wiki/User_Manual')
3762 def on_bug_tracker_activate(self
, widget
, *args
):
3763 if gpodder
.ui
.maemo
:
3764 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3766 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3768 def on_item_support_activate(self
, widget
):
3769 util
.open_website('http://gpodder.org/donate')
3771 def on_itemAbout_activate(self
, widget
, *args
):
3772 if gpodder
.ui
.fremantle
:
3773 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3774 HeAboutDialog
.present(self
.main_window
,
3777 gpodder
.__version
__,
3778 _('A podcast client with focus on usability'),
3779 gpodder
.__copyright
__,
3781 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3782 'http://gpodder.org/donate')
3785 dlg
= gtk
.AboutDialog()
3786 dlg
.set_transient_for(self
.main_window
)
3787 dlg
.set_name('gPodder')
3788 dlg
.set_version(gpodder
.__version
__)
3789 dlg
.set_copyright(gpodder
.__copyright
__)
3790 dlg
.set_comments(_('A podcast client with focus on usability'))
3791 dlg
.set_website(gpodder
.__url
__)
3792 dlg
.set_translator_credits( _('translator-credits'))
3793 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3795 if gpodder
.ui
.desktop
:
3796 # For the "GUI" version, we add some more
3797 # items to the about dialog (credits and logo)
3800 'Thomas Perl <thpinfo.com>',
3803 if os
.path
.exists(gpodder
.credits_file
):
3804 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3805 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3806 app_authors
+= credits
3808 dlg
.set_authors(app_authors
)
3810 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3812 dlg
.set_logo_icon_name('gpodder')
3816 def on_wNotebook_switch_page(self
, widget
, *args
):
3818 if gpodder
.ui
.maemo
:
3819 self
.tool_downloads
.set_active(page_num
== 1)
3820 page
= self
.wNotebook
.get_nth_page(page_num
)
3821 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3822 if page_num
== 0 and self
.active_channel
is not None:
3823 self
.set_title(self
.active_channel
.title
)
3825 self
.set_title(tab_label
)
3827 self
.play_or_download()
3828 self
.menuChannels
.set_sensitive(True)
3829 self
.menuSubscriptions
.set_sensitive(True)
3830 # The message area in the downloads tab should be hidden
3831 # when the user switches away from the downloads tab
3832 if self
.message_area
is not None:
3833 self
.message_area
.hide()
3834 self
.message_area
= None
3836 self
.menuChannels
.set_sensitive(False)
3837 self
.menuSubscriptions
.set_sensitive(False)
3838 if gpodder
.ui
.desktop
:
3839 self
.toolDownload
.set_sensitive(False)
3840 self
.toolPlay
.set_sensitive(False)
3841 self
.toolTransfer
.set_sensitive(False)
3842 self
.toolCancel
.set_sensitive(False)
3844 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3845 # double-click action of the podcast list or enter
3846 self
.treeChannels
.set_cursor(path
)
3848 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3849 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3851 if model
is not None and iter is not None:
3852 old_active_channel
= self
.active_channel
3853 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3855 if self
.active_channel
== old_active_channel
:
3858 if gpodder
.ui
.maemo
:
3859 self
.set_title(self
.active_channel
.title
)
3861 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3862 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3863 self
.itemEditChannel
.set_visible(False)
3864 self
.itemRemoveChannel
.set_visible(False)
3866 self
.itemEditChannel
.set_visible(True)
3867 self
.itemRemoveChannel
.set_visible(True)
3869 self
.active_channel
= None
3870 self
.itemEditChannel
.set_visible(False)
3871 self
.itemRemoveChannel
.set_visible(False)
3873 self
.update_episode_list_model()
3875 def on_btnEditChannel_clicked(self
, widget
, *args
):
3876 self
.on_itemEditChannel_activate( widget
, args
)
3878 def get_podcast_urls_from_selected_episodes(self
):
3879 """Get a set of podcast URLs based on the selected episodes"""
3880 return set(episode
.channel
.url
for episode
in \
3881 self
.get_selected_episodes())
3883 def get_selected_episodes(self
):
3884 """Get a list of selected episodes from treeAvailable"""
3885 selection
= self
.treeAvailable
.get_selection()
3886 model
, paths
= selection
.get_selected_rows()
3888 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3891 def on_transfer_selected_episodes(self
, widget
):
3892 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3894 def on_playback_selected_episodes(self
, widget
):
3895 self
.playback_episodes(self
.get_selected_episodes())
3897 def on_shownotes_selected_episodes(self
, widget
):
3898 episodes
= self
.get_selected_episodes()
3900 episode
= episodes
.pop(0)
3901 self
.show_episode_shownotes(episode
)
3903 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3905 def on_download_selected_episodes(self
, widget
):
3906 episodes
= self
.get_selected_episodes()
3907 self
.download_episode_list(episodes
)
3908 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3909 self
.play_or_download()
3911 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3912 """Double-click/enter action handler for treeAvailable"""
3913 # We should only have one one selected as it was double clicked!
3914 e
= self
.get_selected_episodes()[0]
3916 if (self
.config
.double_click_episode_action
== 'download'):
3917 # If the episode has already been downloaded and exists then play it
3918 if e
.was_downloaded(and_exists
=True):
3919 self
.playback_episodes(self
.get_selected_episodes())
3920 # else download it if it is not already downloading
3921 elif not self
.episode_is_downloading(e
):
3922 self
.download_episode_list([e
])
3923 self
.update_episode_list_icons([e
.url
])
3924 self
.play_or_download()
3925 elif (self
.config
.double_click_episode_action
== 'stream'):
3926 # If we happen to have downloaded this episode simple play it
3927 if e
.was_downloaded(and_exists
=True):
3928 self
.playback_episodes(self
.get_selected_episodes())
3929 # else if streaming is possible stream it
3930 elif self
.streaming_possible():
3931 self
.playback_episodes(self
.get_selected_episodes())
3933 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3934 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3936 # default action is to display show notes
3937 self
.on_shownotes_selected_episodes(widget
)
3939 def show_episode_shownotes(self
, episode
):
3940 if self
.episode_shownotes_window
is None:
3941 log('First-time use of episode window --- creating', sender
=self
)
3942 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3943 _download_episode_list
=self
.download_episode_list
, \
3944 _playback_episodes
=self
.playback_episodes
, \
3945 _delete_episode_list
=self
.delete_episode_list
, \
3946 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3947 _cancel_task_list
=self
.cancel_task_list
, \
3948 _episode_is_downloading
=self
.episode_is_downloading
, \
3949 _streaming_possible
=self
.streaming_possible())
3950 self
.episode_shownotes_window
.show(episode
)
3951 if self
.episode_is_downloading(episode
):
3952 self
.update_downloads_list()
3954 def restart_auto_update_timer(self
):
3955 if self
._auto
_update
_timer
_source
_id
is not None:
3956 log('Removing existing auto update timer.', sender
=self
)
3957 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
3958 self
._auto
_update
_timer
_source
_id
= None
3960 if self
.config
.auto_update_feeds
and \
3961 self
.config
.auto_update_frequency
:
3962 interval
= 60*1000*self
.config
.auto_update_frequency
3963 log('Setting up auto update timer with interval %d.', \
3964 self
.config
.auto_update_frequency
, sender
=self
)
3965 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
3966 interval
, self
._on
_auto
_update
_timer
)
3968 def _on_auto_update_timer(self
):
3969 log('Auto update timer fired.', sender
=self
)
3970 self
.update_feed_cache(force_update
=True)
3972 # Ask web service for sub changes (if enabled)
3973 self
.mygpo_client
.flush()
3977 def on_treeDownloads_row_activated(self
, widget
, *args
):
3978 # Use the standard way of working on the treeview
3979 selection
= self
.treeDownloads
.get_selection()
3980 (model
, paths
) = selection
.get_selected_rows()
3981 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
3983 for tree_row_reference
, task
in selected_tasks
:
3984 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
3985 task
.status
= task
.PAUSED
3986 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
3987 self
.download_queue_manager
.add_task(task
)
3988 self
.enable_download_list_update()
3989 elif task
.status
== task
.DONE
:
3990 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
3992 self
.play_or_download()
3994 # Update the tab title and downloads list
3995 self
.update_downloads_list()
3997 def on_item_cancel_download_activate(self
, widget
):
3998 if self
.wNotebook
.get_current_page() == 0:
3999 selection
= self
.treeAvailable
.get_selection()
4000 (model
, paths
) = selection
.get_selected_rows()
4001 urls
= [model
.get_value(model
.get_iter(path
), \
4002 self
.episode_list_model
.C_URL
) for path
in paths
]
4003 selected_tasks
= [task
for task
in self
.download_tasks_seen \
4004 if task
.url
in urls
]
4006 selection
= self
.treeDownloads
.get_selection()
4007 (model
, paths
) = selection
.get_selected_rows()
4008 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
4009 self
.download_status_model
.C_TASK
) for path
in paths
]
4010 self
.cancel_task_list(selected_tasks
)
4012 def on_btnCancelAll_clicked(self
, widget
, *args
):
4013 self
.cancel_task_list(self
.download_tasks_seen
)
4015 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
4016 episodes
= self
.get_selected_episodes()
4017 if len(episodes
) == 1:
4018 self
.delete_episode_list(episodes
, skip_locked
=False)
4020 self
.delete_episode_list(episodes
)
4022 def on_key_press(self
, widget
, event
):
4023 # Allow tab switching with Ctrl + PgUp/PgDown
4024 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
4025 if event
.keyval
== gtk
.keysyms
.Page_Up
:
4026 self
.wNotebook
.prev_page()
4028 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
4029 self
.wNotebook
.next_page()
4032 # After this code we only handle Maemo hardware keys,
4033 # so if we are not a Maemo app, we don't do anything
4034 if not gpodder
.ui
.maemo
:
4038 if event
.keyval
== gtk
.keysyms
.F7
: #plus
4040 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
4043 if diff
!= 0 and not self
.currently_updating
:
4044 selection
= self
.treeChannels
.get_selection()
4045 (model
, iter) = selection
.get_selected()
4046 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
4047 selection
.select_path(new_path
)
4048 self
.treeChannels
.set_cursor(new_path
)
4053 def on_iconify(self
):
4055 self
.gPodder
.set_skip_taskbar_hint(True)
4056 if self
.config
.minimize_to_tray
:
4057 self
.tray_icon
.set_visible(True)
4059 self
.gPodder
.set_skip_taskbar_hint(False)
4061 def on_uniconify(self
):
4063 self
.gPodder
.set_skip_taskbar_hint(False)
4064 if self
.config
.minimize_to_tray
:
4065 self
.tray_icon
.set_visible(False)
4067 self
.gPodder
.set_skip_taskbar_hint(False)
4069 def uniconify_main_window(self
):
4070 if self
.is_iconified():
4071 # We need to hide and then show the window in WMs like Metacity
4072 # or KWin4 to move the window to the active workspace
4073 # (see http://gpodder.org/bug/1125)
4076 self
.gPodder
.present()
4078 def iconify_main_window(self
):
4079 if not self
.is_iconified():
4080 self
.gPodder
.iconify()
4082 def update_podcasts_tab(self
):
4083 if len(self
.channels
):
4084 if gpodder
.ui
.fremantle
:
4085 self
.button_refresh
.set_title(_('Check for new episodes'))
4086 self
.button_refresh
.show()
4088 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
4090 if gpodder
.ui
.fremantle
:
4091 self
.button_refresh
.hide()
4093 self
.label2
.set_text(_('Podcasts'))
4095 @dbus.service
.method(gpodder
.dbus_interface
)
4096 def show_gui_window(self
):
4097 parent
= self
.get_dialog_parent()
4100 @dbus.service
.method(gpodder
.dbus_interface
)
4101 def subscribe_to_url(self
, url
):
4102 gPodderAddPodcast(self
.gPodder
,
4103 add_urls_callback
=self
.add_podcast_list
,
4106 @dbus.service
.method(gpodder
.dbus_interface
)
4107 def mark_episode_played(self
, filename
):
4108 if filename
is None:
4111 for channel
in self
.channels
:
4112 for episode
in channel
.get_all_episodes():
4113 fn
= episode
.local_filename(create
=False, check_only
=True)
4115 episode
.mark(is_played
=True)
4117 self
.update_episode_list_icons([episode
.url
])
4118 self
.update_podcast_list_model([episode
.channel
.url
])
4124 def main(options
=None):
4125 gobject
.threads_init()
4126 gobject
.set_application_name('gPodder')
4128 if gpodder
.ui
.maemo
:
4129 # Try to enable the custom icon theme for gPodder on Maemo
4130 settings
= gtk
.settings_get_default()
4131 settings
.set_string_property('gtk-icon-theme-name', \
4132 'gpodder', __file__
)
4133 # Extend the search path for the optified icon theme (Maemo 5)
4134 icon_theme
= gtk
.icon_theme_get_default()
4135 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
4137 gtk
.window_set_default_icon_name('gpodder')
4138 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
4141 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
4142 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
4144 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
4145 except dbus
.exceptions
.DBusException
, dbe
:
4146 log('Warning: Cannot get "on the bus".', traceback
=True)
4147 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
4148 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
4149 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
4150 dlg
.set_title('gPodder')
4155 util
.make_directory(gpodder
.home
)
4156 gpodder
.load_plugins()
4158 config
= UIConfig(gpodder
.config_file
)
4160 # Load hook modules and install the hook manager globally
4161 # if modules have been found an instantiated by the manager
4162 user_hooks
= hooks
.HookManager()
4163 if user_hooks
.has_modules():
4164 gpodder
.user_hooks
= user_hooks
4166 if gpodder
.ui
.diablo
:
4167 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4168 # folder exists there (allow moving "gpodder" between SD cards or USB)
4169 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4170 if not os
.path
.exists(config
.download_dir
):
4171 log('Downloads might have been moved. Trying to locate them...')
4172 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4173 dir = os
.path
.join(basedir
, 'gpodder')
4174 if os
.path
.exists(dir):
4175 log('Downloads found in: %s', dir)
4176 config
.download_dir
= dir
4179 log('Downloads NOT FOUND in %s', dir)
4180 elif gpodder
.ui
.fremantle
:
4181 config
.on_quit_ask
= False
4183 if config
.enable_fingerscroll
:
4184 BuilderWidget
.use_fingerscroll
= True
4186 config
.mygpo_device_type
= util
.detect_device_type()
4188 gp
= gPodder(bus_name
, config
)
4191 if options
.subscribe
:
4192 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4195 # handle "subscribe to podcast" events from firefox
4196 if platform
.system() == 'Darwin':
4197 from gpodder
import gpodderosx
4198 gpodderosx
.register_handlers(gp
)
4199 # end mac OS X stuff