1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2011 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/>.
47 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
50 def __init__(self
, *args
, **kwargs
):
52 def add_signal_receiver(self
, *args
, **kwargs
):
56 def __init__(self
, *args
, **kwargs
):
60 def method(*args
, **kwargs
):
63 def __init__(self
, *args
, **kwargs
):
66 def __init__(self
, *args
, **kwargs
):
69 from gpodder
import core
70 from gpodder
import feedcore
71 from gpodder
import util
72 from gpodder
import opml
73 from gpodder
import download
74 from gpodder
import my
75 from gpodder
import youtube
76 from gpodder
import player
77 from gpodder
.liblogger
import log
82 from gpodder
.gtkui
.model
import Model
83 from gpodder
.gtkui
.model
import PodcastListModel
84 from gpodder
.gtkui
.model
import EpisodeListModel
85 from gpodder
.gtkui
.config
import UIConfig
86 from gpodder
.gtkui
.services
import CoverDownloader
87 from gpodder
.gtkui
.widgets
import SimpleMessageArea
88 from gpodder
.gtkui
.desktopfile
import UserAppsReader
90 from gpodder
.gtkui
.draw
import draw_text_box_centered
92 from gpodder
.gtkui
.interface
.common
import BuilderWidget
93 from gpodder
.gtkui
.interface
.common
import TreeViewHelper
94 from gpodder
.gtkui
.interface
.addpodcast
import gPodderAddPodcast
96 if gpodder
.ui
.desktop
:
97 from gpodder
.gtkui
.download
import DownloadStatusModel
99 from gpodder
.gtkui
.desktop
.welcome
import gPodderWelcome
100 from gpodder
.gtkui
.desktop
.channel
import gPodderChannel
101 from gpodder
.gtkui
.desktop
.preferences
import gPodderPreferences
102 from gpodder
.gtkui
.desktop
.shownotes
import gPodderShownotes
103 from gpodder
.gtkui
.desktop
.episodeselector
import gPodderEpisodeSelector
104 from gpodder
.gtkui
.desktop
.podcastdirectory
import gPodderPodcastDirectory
105 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
106 elif gpodder
.ui
.fremantle
:
107 from gpodder
.gtkui
.frmntl
.model
import DownloadStatusModel
108 from gpodder
.gtkui
.frmntl
.model
import EpisodeListModel
109 from gpodder
.gtkui
.frmntl
.model
import PodcastListModel
111 from gpodder
.gtkui
.frmntl
.preferences
import gPodderPreferences
112 from gpodder
.gtkui
.frmntl
.shownotes
import gPodderShownotes
113 from gpodder
.gtkui
.frmntl
.episodeselector
import gPodderEpisodeSelector
114 from gpodder
.gtkui
.frmntl
.podcastdirectory
import gPodderPodcastDirectory
115 from gpodder
.gtkui
.frmntl
.episodes
import gPodderEpisodes
116 from gpodder
.gtkui
.frmntl
.downloads
import gPodderDownloads
117 from gpodder
.gtkui
.frmntl
.progress
import ProgressIndicator
118 from gpodder
.gtkui
.frmntl
.widgets
import FancyProgressBar
120 from gpodder
.gtkui
.frmntl
.portrait
import FremantleRotation
121 from gpodder
.gtkui
.frmntl
.mafw
import MafwPlaybackMonitor
122 from gpodder
.gtkui
.frmntl
.hints
import HINT_STRINGS
123 from gpodder
.gtkui
.frmntl
.network
import NetworkManager
125 from gpodder
.gtkui
.interface
.common
import Orientation
127 if gpodder
.ui
.fremantle
:
130 from gpodder
.dbusproxy
import DBusPodcastsProxy
131 from gpodder
import hooks
133 class gPodder(BuilderWidget
, dbus
.service
.Object
):
134 ICON_GENERAL_ADD
= 'general_add'
135 ICON_GENERAL_REFRESH
= 'general_refresh'
137 # Delay until live search is started after typing stop
138 LIVE_SEARCH_DELAY
= 500
140 def __init__(self
, bus_name
, gpodder_core
):
141 dbus
.service
.Object
.__init
__(self
, object_path
=gpodder
.dbus_gui_object_path
, bus_name
=bus_name
)
142 self
.podcasts_proxy
= DBusPodcastsProxy(lambda: self
.channels
, \
143 self
.on_itemUpdate_activate
, \
144 self
.playback_episodes
, \
145 self
.download_episode_list
, \
146 self
.episode_object_by_uri
, \
148 self
.core
= gpodder_core
149 self
.config
= self
.core
.config
150 self
.db
= self
.core
.db
151 BuilderWidget
.__init
__(self
, None)
154 if gpodder
.ui
.fremantle
:
156 self
.app
= hildon
.Program()
157 self
.app
.add_window(self
.main_window
)
159 appmenu
= hildon
.AppMenu()
161 for filter in (self
.item_view_podcasts_all
, \
162 self
.item_view_podcasts_downloaded
, \
163 self
.item_view_podcasts_unplayed
):
164 button
= gtk
.ToggleButton()
165 filter.connect_proxy(button
)
166 appmenu
.add_filter(button
)
168 for action
in (self
.itemPreferences
, \
169 self
.item_downloads
, \
170 self
.itemRemoveOldEpisodes
, \
171 self
.item_unsubscribe
, \
173 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
174 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
175 action
.connect_proxy(button
)
176 if action
== self
.item_downloads
:
177 button
.set_title(_('Downloads'))
178 button
.set_value(_('Idle'))
179 self
.button_downloads
= button
180 appmenu
.append(button
)
182 def show_hint(button
):
183 self
.show_message(random
.choice(HINT_STRINGS
), important
=True)
185 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
186 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
187 button
.set_title(_('Hint of the day'))
188 button
.connect('clicked', show_hint
)
189 appmenu
.append(button
)
192 self
.main_window
.set_app_menu(appmenu
)
194 # Initialize portrait mode / rotation manager
195 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
197 gpodder
.__version
__, \
198 self
.config
.rotation_mode
)
200 # Initialize the Fremantle network manager
201 self
.network_manager
= NetworkManager()
203 if self
.config
.rotation_mode
== FremantleRotation
.ALWAYS
:
204 util
.idle_add(self
.on_window_orientation_changed
, \
205 Orientation
.PORTRAIT
)
206 self
._last
_orientation
= Orientation
.PORTRAIT
208 self
._last
_orientation
= Orientation
.LANDSCAPE
210 # Flag set when a notification is being shown (Maemo bug 11235)
211 self
._fremantle
_notification
_visible
= False
213 self
._last
_orientation
= Orientation
.LANDSCAPE
214 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
216 self
.bluetooth_available
= util
.bluetooth_available()
218 if not gpodder
.ui
.fremantle
:
219 self
.config
.connect_gtk_window(self
.gPodder
, '_main_window')
221 # Default/last paned position for sidebar toggling
222 self
._last
_paned
_position
= 200
223 self
._last
_paned
_position
_toggling
= False
224 self
.item_sidebar
.set_active(self
.config
._paned
_position
> 0)
226 self
.config
.connect_gtk_paned('_paned_position', self
.channelPaned
)
228 self
.main_window
.show()
230 self
.player_receiver
= player
.MediaPlayerDBusReceiver(self
.on_played
)
232 if gpodder
.ui
.fremantle
:
233 # Create a D-Bus monitoring object that takes care of
234 # tracking MAFW (Nokia Media Player) playback events
235 # and sends episode playback status events via D-Bus
236 self
.mafw_monitor
= MafwPlaybackMonitor(gpodder
.dbus_session_bus
)
238 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
240 self
.preferences_dialog
= None
241 self
.episode_columns_menu
= None
242 self
.config
.add_observer(self
.on_config_changed
)
244 self
.episode_shownotes_window
= None
245 self
.new_episodes_window
= None
247 if gpodder
.ui
.desktop
:
248 # Mac OS X-specific UI tweaks: Native main menu integration
249 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
250 if getattr(gtk
.gdk
, 'WINDOWING', 'x11') == 'quartz':
252 import igemacintegration
as igemi
254 # Move the menu bar from the window to the Mac menu bar
256 igemi
.ige_mac_menu_set_menu_bar(self
.mainMenu
)
258 # Reparent some items to the "Application" menu
259 for widget
in ('/mainMenu/menuHelp/itemAbout', \
260 '/mainMenu/menuPodcasts/itemPreferences'):
261 item
= self
.uimanager1
.get_widget(widget
)
262 group
= igemi
.ige_mac_menu_add_app_menu_group()
263 igemi
.ige_mac_menu_add_app_menu_item(group
, item
, None)
265 quit_widget
= '/mainMenu/menuPodcasts/itemQuit'
266 quit_item
= self
.uimanager1
.get_widget(quit_widget
)
267 igemi
.ige_mac_menu_set_quit_menu_item(quit_item
)
269 print >>sys
.stderr
, """
270 Warning: ige-mac-integration not found - no native menus.
273 self
.download_status_model
= DownloadStatusModel()
274 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
276 if gpodder
.ui
.desktop
:
277 self
.itemShowAllEpisodes
.set_active(self
.config
.podcast_list_view_all
)
278 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
279 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
281 if not gpodder
.ui
.fremantle
:
282 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
283 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
284 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
285 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
287 # When the amount of maximum downloads changes, notify the queue manager
288 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_threads()
289 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
291 self
.default_title
= 'gPodder'
292 if gpodder
.__version
__.rfind('git') != -1:
293 self
.set_title('gPodder %s' % gpodder
.__version
__)
295 title
= self
.gPodder
.get_title()
296 if title
is not None:
297 self
.set_title(title
)
299 self
.set_title(_('gPodder'))
301 self
.cover_downloader
= CoverDownloader()
303 # Generate list models for podcasts and their episodes
304 self
.podcast_list_model
= PodcastListModel(self
.cover_downloader
)
306 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
307 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
309 if gpodder
.ui
.fremantle
:
310 # Work around Maemo bug #4718
311 self
.button_refresh
.set_name('HildonButton-finger')
312 self
.button_subscribe
.set_name('HildonButton-finger')
314 self
.button_refresh
.set_sensitive(False)
315 self
.button_subscribe
.set_sensitive(False)
317 self
.button_subscribe
.set_image(gtk
.image_new_from_icon_name(\
318 self
.ICON_GENERAL_ADD
, gtk
.ICON_SIZE_BUTTON
))
319 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
320 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
322 # Make the button scroll together with the TreeView contents
323 action_area_box
= self
.treeChannels
.get_action_area_box()
324 for child
in self
.buttonbox
:
325 child
.reparent(action_area_box
)
326 self
.vbox
.remove(self
.buttonbox
)
327 self
.treeChannels
.set_action_area_visible(True)
329 # Set up a very nice progress bar setup
330 self
.fancy_progress_bar
= FancyProgressBar(self
.main_window
, \
331 self
.on_btnCancelFeedUpdate_clicked
)
332 self
.pbFeedUpdate
= self
.fancy_progress_bar
.progress_bar
333 self
.pbFeedUpdate
.set_ellipsize(pango
.ELLIPSIZE_MIDDLE
)
334 self
.vbox
.pack_start(self
.fancy_progress_bar
.event_box
, False)
336 from gpodder
.gtkui
.frmntl
import style
337 sub_font
= style
.get_font_desc('SmallSystemFont')
338 sub_color
= style
.get_color('SecondaryTextColor')
339 sub
= (sub_font
.to_string(), sub_color
.to_string())
340 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
341 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
343 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
344 while gtk
.events_pending():
345 gtk
.main_iteration(False)
347 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
348 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
349 show_episode_shownotes
=self
.show_episode_shownotes
, \
350 update_podcast_list_model
=self
.update_podcast_list_model
, \
351 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
352 item_view_episodes_all
=self
.item_view_episodes_all
, \
353 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
354 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
355 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
356 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
357 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
358 hide_episode_search
=self
.hide_episode_search
, \
359 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
360 playback_episodes
=self
.playback_episodes
, \
361 delete_episode_list
=self
.delete_episode_list
, \
362 episode_list_status_changed
=self
.episode_list_status_changed
, \
363 download_episode_list
=self
.download_episode_list
, \
364 episode_is_downloading
=self
.episode_is_downloading
, \
365 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
366 add_download_task_monitor
=self
.add_download_task_monitor
, \
367 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
368 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
, \
369 on_itemUpdate_activate
=self
.on_itemUpdate_activate
, \
370 show_delete_episodes_window
=self
.show_delete_episodes_window
, \
371 cover_downloader
=self
.cover_downloader
)
373 # Expose objects for episode list type-ahead find
374 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
375 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
376 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
378 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
379 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
380 cleanup_downloads
=self
.cleanup_downloads
, \
381 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
382 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
385 self
.treeAvailable
= self
.episodes_window
.treeview
386 self
.treeDownloads
= self
.downloads_window
.treeview
388 # Source IDs for timeouts for search-as-you-type
389 self
._podcast
_list
_search
_timeout
= None
390 self
._episode
_list
_search
_timeout
= None
392 # Init the treeviews that we use
393 self
.init_podcast_list_treeview()
394 self
.init_episode_list_treeview()
395 self
.init_download_list_treeview()
397 if self
.config
.podcast_list_hide_boring
:
398 self
.item_view_hide_boring_podcasts
.set_active(True)
400 self
.currently_updating
= False
402 self
.context_menu_mouse_button
= 3
404 self
.download_tasks_seen
= set()
405 self
.download_list_update_enabled
= False
406 self
.download_task_monitors
= set()
408 # Subscribed channels
409 self
.active_channel
= None
410 self
.channels
= Model
.get_podcasts(self
.db
)
412 # Check if the user has downloaded any podcast with an external program
413 # and mark episodes as downloaded / move them away (bug 902)
414 for podcast
in self
.channels
:
415 podcast
.import_external_files()
417 self
.channel_list_changed
= True
419 # load list of user applications for audio playback
420 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
421 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
423 # Set up the first instance of MygPoClient
424 self
.mygpo_client
= my
.MygPoClient(self
.config
)
426 # Now, update the feed cache, when everything's in place
427 if not gpodder
.ui
.fremantle
:
428 self
.btnUpdateFeeds
.show()
429 self
.updating_feed_cache
= False
430 self
.feed_cache_update_cancelled
= False
431 self
.update_feed_cache(force_update
=False)
433 self
.message_area
= None
435 def find_partial_downloads():
436 # Look for partial file downloads
437 partial_files
= glob
.glob(os
.path
.join(gpodder
.downloads
, '*', '*.partial'))
438 count
= len(partial_files
)
439 resumable_episodes
= []
441 if not gpodder
.ui
.fremantle
:
442 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
443 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
444 _('Some episodes have not finished downloading in a previous session.'), \
445 False, self
.get_dialog_parent())
446 indicator
.on_message(N_('%(count)d partial file', '%(count)d partial files', count
) % {'count':count
})
448 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
451 for c
in self
.channels
:
452 for e
in c
.get_all_episodes():
453 filename
= e
.local_filename(create
=False, check_only
=True)
454 if filename
in candidates
:
455 log('Found episode: %s', e
.title
, sender
=self
)
457 indicator
.on_message(e
.title
)
458 indicator
.on_progress(float(found
)/count
)
459 candidates
.remove(filename
)
460 partial_files
.remove(filename
+'.partial')
462 if os
.path
.exists(filename
):
463 # The file has already been downloaded;
464 # remove the leftover partial file
465 util
.delete_file(filename
+'.partial')
467 resumable_episodes
.append(e
)
475 for f
in partial_files
:
476 log('Partial file without episode: %s', f
, sender
=self
)
479 util
.idle_add(indicator
.on_finished
)
481 if len(resumable_episodes
):
482 def offer_resuming():
483 self
.download_episode_list_paused(resumable_episodes
)
484 if not gpodder
.ui
.fremantle
:
485 resume_all
= gtk
.Button(_('Resume all'))
486 #resume_all.set_border_width(0)
487 def on_resume_all(button
):
488 selection
= self
.treeDownloads
.get_selection()
489 selection
.select_all()
490 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
491 selection
.unselect_all()
492 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
493 self
.message_area
.hide()
494 resume_all
.connect('clicked', on_resume_all
)
496 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
497 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
498 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
499 self
.message_area
.show_all()
500 self
.clean_up_downloads(delete_partial
=False)
501 util
.idle_add(offer_resuming
)
502 elif not gpodder
.ui
.fremantle
:
503 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
505 util
.idle_add(self
.clean_up_downloads
, True)
506 threading
.Thread(target
=find_partial_downloads
).start()
508 # Start the auto-update procedure
509 self
._auto
_update
_timer
_source
_id
= None
510 if self
.config
.auto_update_feeds
:
511 self
.restart_auto_update_timer()
513 # Delete old episodes if the user wishes to
514 if self
.config
.auto_remove_played_episodes
and \
515 self
.config
.episode_old_age
> 0:
516 old_episodes
= list(self
.get_expired_episodes())
517 if len(old_episodes
) > 0:
518 self
.delete_episode_list(old_episodes
, confirm
=False)
519 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
521 if gpodder
.ui
.fremantle
:
522 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
523 self
.button_refresh
.set_sensitive(True)
524 self
.button_subscribe
.set_sensitive(True)
525 self
.main_window
.set_title(_('gPodder'))
526 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
528 # Do the initial sync with the web service
529 util
.idle_add(self
.mygpo_client
.flush
, True)
531 # First-time users should be asked if they want to see the OPML
532 if not self
.channels
and not gpodder
.ui
.fremantle
:
533 util
.idle_add(self
.on_itemUpdate_activate
)
535 def on_view_sidebar_toggled(self
, menu_item
):
536 self
.channelPaned
.child_set_property(self
.vboxChannelNavigator
, \
537 'shrink', not menu_item
.get_active())
539 if self
._last
_paned
_position
_toggling
:
542 active
= menu_item
.get_active()
544 if self
._last
_paned
_position
== 0:
545 self
._last
_paned
_position
= 200
546 self
.channelPaned
.set_position(self
._last
_paned
_position
)
548 current_position
= self
.channelPaned
.get_position()
549 if current_position
> 0:
550 self
._last
_paned
_position
= current_position
551 self
.channelPaned
.set_position(0)
553 def episode_object_by_uri(self
, uri
):
554 """Get an episode object given a local or remote URI
556 This can be used to quickly access an episode object
557 when all we have is its download filename or episode
558 URL (e.g. from external D-Bus calls / signals, etc..)
560 if uri
.startswith('/'):
561 uri
= 'file://' + urllib
.quote(uri
)
563 prefix
= 'file://' + urllib
.quote(gpodder
.downloads
)
565 if uri
.startswith(prefix
):
566 # File is on the local filesystem in the download folder
567 filename
= urllib
.unquote(uri
[len(prefix
):])
568 file_parts
= [x
for x
in filename
.split(os
.sep
) if x
]
570 if len(file_parts
) == 2:
571 dir_name
, filename
= file_parts
572 channels
= [c
for c
in self
.channels
if c
.download_folder
== dir_name
]
573 if len(channels
) == 1:
574 channel
= channels
[0]
575 return channel
.get_episode_by_filename(filename
)
577 # Possibly remote file - search the database for a podcast
578 channel_id
= self
.db
.get_podcast_id_from_episode_url(uri
)
580 if channel_id
is not None:
581 channels
= [c
for c
in self
.channels
if c
.id == channel_id
]
582 if len(channels
) == 1:
583 channel
= channels
[0]
584 return channel
.get_episode_by_url(uri
)
588 def on_played(self
, start
, end
, total
, file_uri
):
589 """Handle the "played" signal from a media player"""
590 if start
== 0 and end
== 0 and total
== 0:
591 # Ignore bogus play event
593 elif end
< start
+ 5:
594 # Ignore "less than five seconds" segments,
595 # as they can happen with seeking, etc...
598 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
599 episode
= self
.episode_object_by_uri(file_uri
)
601 if episode
is not None:
602 file_type
= episode
.file_type()
606 episode
.total_time
= total
608 # Assume the episode's total time for the action
609 total
= episode
.total_time
610 if episode
.current_position_updated
is None or \
611 now
> episode
.current_position_updated
:
612 episode
.current_position
= end
613 episode
.current_position_updated
= now
614 episode
.mark(is_played
=True)
617 self
.update_episode_list_icons([episode
.url
])
618 self
.update_podcast_list_model([episode
.channel
.url
])
620 # Submit this action to the webservice
621 self
.mygpo_client
.on_playback_full(episode
, \
624 def on_add_remove_podcasts_mygpo(self
):
625 actions
= self
.mygpo_client
.get_received_actions()
629 existing_urls
= [c
.url
for c
in self
.channels
]
631 # Columns for the episode selector window - just one...
633 ('description', None, None, _('Action')),
636 # A list of actions that have to be chosen from
639 # Actions that are ignored (already carried out)
642 for action
in actions
:
643 if action
.is_add
and action
.url
not in existing_urls
:
644 changes
.append(my
.Change(action
))
645 elif action
.is_remove
and action
.url
in existing_urls
:
646 podcast_object
= None
647 for podcast
in self
.channels
:
648 if podcast
.url
== action
.url
:
649 podcast_object
= podcast
651 changes
.append(my
.Change(action
, podcast_object
))
653 log('Ignoring action: %s', action
, sender
=self
)
654 ignored
.append(action
)
656 # Confirm all ignored changes
657 self
.mygpo_client
.confirm_received_actions(ignored
)
659 def execute_podcast_actions(selected
):
660 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
661 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
663 # Apply the accepted changes locally
664 self
.add_podcast_list(add_list
)
665 self
.remove_podcast_list(remove_list
, confirm
=False)
667 # All selected items are now confirmed
668 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
670 # Revert the changes on the server
671 rejected
= [c
.action
for c
in changes
if c
not in selected
]
672 self
.mygpo_client
.reject_received_actions(rejected
)
675 # We're abusing the Episode Selector again ;) -- thp
676 gPodderEpisodeSelector(self
.main_window
, \
677 title
=_('Confirm changes from gpodder.net'), \
678 instructions
=_('Select the actions you want to carry out.'), \
681 size_attribute
=None, \
682 stock_ok_button
=gtk
.STOCK_APPLY
, \
683 callback
=execute_podcast_actions
, \
686 # There are some actions that need the user's attention
691 # We have no remaining actions - no selection happens
694 def rewrite_urls_mygpo(self
):
695 # Check if we have to rewrite URLs since the last add
696 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
698 for rewritten_url
in rewritten_urls
:
699 if not rewritten_url
.new_url
:
702 for channel
in self
.channels
:
703 if channel
.url
== rewritten_url
.old_url
:
704 log('Updating URL of %s to %s', channel
, \
705 rewritten_url
.new_url
, sender
=self
)
706 channel
.url
= rewritten_url
.new_url
708 self
.channel_list_changed
= True
709 util
.idle_add(self
.update_episode_list_model
)
712 def on_send_full_subscriptions(self
):
713 # Send the full subscription list to the gpodder.net client
714 # (this will overwrite the subscription list on the server)
715 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
716 _('Your subscriptions are being uploaded to the server.'), \
717 False, self
.get_dialog_parent())
720 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
721 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
726 message
= e
.__class
__.__name
__
727 self
.show_message(message
, \
728 _('Error while uploading'), \
730 util
.idle_add(show_error
, e
)
732 util
.idle_add(indicator
.on_finished
)
734 def on_podcast_selected(self
, treeview
, path
, column
):
736 model
= treeview
.get_model()
737 channel
= model
.get_value(model
.get_iter(path
), \
738 PodcastListModel
.C_CHANNEL
)
739 self
.active_channel
= channel
740 self
.update_episode_list_model()
741 self
.episodes_window
.channel
= self
.active_channel
742 self
.episodes_window
.show()
744 def on_button_subscribe_clicked(self
, button
):
745 self
.on_itemImportChannels_activate(button
)
747 def on_button_downloads_clicked(self
, widget
):
748 self
.downloads_window
.show()
750 def show_episode_in_download_manager(self
, episode
):
751 self
.downloads_window
.show()
752 model
= self
.treeDownloads
.get_model()
753 selection
= self
.treeDownloads
.get_selection()
754 selection
.unselect_all()
755 it
= model
.get_iter_first()
756 while it
is not None:
757 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
758 if task
.episode
.url
== episode
.url
:
759 selection
.select_iter(it
)
760 # FIXME: Scroll to selection in pannable area
762 it
= model
.iter_next(it
)
764 def for_each_episode_set_task_status(self
, episodes
, status
):
765 episode_urls
= set(episode
.url
for episode
in episodes
)
766 model
= self
.treeDownloads
.get_model()
767 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
768 model
.get_value(row
.iter, \
769 DownloadStatusModel
.C_TASK
)) for row
in model \
770 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
772 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
774 def on_window_orientation_changed(self
, orientation
):
775 self
._last
_orientation
= orientation
776 if self
.preferences_dialog
is not None:
777 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
779 treeview
= self
.treeChannels
780 if orientation
== Orientation
.PORTRAIT
:
781 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
782 # Work around Maemo bug #4718
783 self
.button_subscribe
.set_name('HildonButton-thumb')
784 self
.button_refresh
.set_name('HildonButton-thumb')
786 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
787 # Work around Maemo bug #4718
788 self
.button_subscribe
.set_name('HildonButton-finger')
789 self
.button_refresh
.set_name('HildonButton-finger')
791 if gpodder
.ui
.fremantle
:
792 self
.fancy_progress_bar
.relayout()
794 def on_treeview_podcasts_selection_changed(self
, selection
):
795 model
, iter = selection
.get_selected()
797 self
.active_channel
= None
798 self
.episode_list_model
.clear()
800 def on_treeview_button_pressed(self
, treeview
, event
):
801 if event
.window
!= treeview
.get_bin_window():
804 TreeViewHelper
.save_button_press_event(treeview
, event
)
806 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
807 TreeViewHelper
.ROLE_PODCASTS
:
808 return self
.currently_updating
810 return event
.button
== self
.context_menu_mouse_button
and \
813 def on_treeview_podcasts_button_released(self
, treeview
, event
):
814 if event
.window
!= treeview
.get_bin_window():
817 return self
.treeview_channels_show_context_menu(treeview
, event
)
819 def on_treeview_episodes_button_released(self
, treeview
, event
):
820 if event
.window
!= treeview
.get_bin_window():
823 return self
.treeview_available_show_context_menu(treeview
, event
)
825 def on_treeview_downloads_button_released(self
, treeview
, event
):
826 if event
.window
!= treeview
.get_bin_window():
829 return self
.treeview_downloads_show_context_menu(treeview
, event
)
831 def on_entry_search_podcasts_changed(self
, editable
):
832 if self
.hbox_search_podcasts
.get_property('visible'):
833 def set_search_term(self
, text
):
834 self
.podcast_list_model
.set_search_term(text
)
835 self
._podcast
_list
_search
_timeout
= None
838 if self
._podcast
_list
_search
_timeout
is not None:
839 gobject
.source_remove(self
._podcast
_list
_search
_timeout
)
840 self
._podcast
_list
_search
_timeout
= gobject
.timeout_add(\
841 self
.LIVE_SEARCH_DELAY
, \
842 set_search_term
, self
, editable
.get_chars(0, -1))
844 def on_entry_search_podcasts_key_press(self
, editable
, event
):
845 if event
.keyval
== gtk
.keysyms
.Escape
:
846 self
.hide_podcast_search()
849 def hide_podcast_search(self
, *args
):
850 if self
._podcast
_list
_search
_timeout
is not None:
851 gobject
.source_remove(self
._podcast
_list
_search
_timeout
)
852 self
._podcast
_list
_search
_timeout
= None
853 self
.hbox_search_podcasts
.hide()
854 self
.entry_search_podcasts
.set_text('')
855 self
.podcast_list_model
.set_search_term(None)
856 self
.treeChannels
.grab_focus()
858 def show_podcast_search(self
, input_char
):
859 self
.hbox_search_podcasts
.show()
860 self
.entry_search_podcasts
.insert_text(input_char
, -1)
861 self
.entry_search_podcasts
.grab_focus()
862 self
.entry_search_podcasts
.set_position(-1)
864 def init_podcast_list_treeview(self
):
865 # Set up podcast channel tree view widget
866 if gpodder
.ui
.fremantle
:
867 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
868 self
.item_view_podcasts_downloaded
.set_active(True)
869 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
870 self
.item_view_podcasts_unplayed
.set_active(True)
872 self
.item_view_podcasts_all
.set_active(True)
873 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
875 iconcolumn
= gtk
.TreeViewColumn('')
876 iconcell
= gtk
.CellRendererPixbuf()
877 iconcolumn
.pack_start(iconcell
, False)
878 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
879 self
.treeChannels
.append_column(iconcolumn
)
881 namecolumn
= gtk
.TreeViewColumn('')
882 namecell
= gtk
.CellRendererText()
883 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
884 namecolumn
.pack_start(namecell
, True)
885 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
887 if gpodder
.ui
.fremantle
:
888 countcell
= gtk
.CellRendererText()
889 from gpodder
.gtkui
.frmntl
import style
890 countcell
.set_property('font-desc', style
.get_font_desc('EmpSystemFont'))
891 countcell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
892 countcell
.set_property('alignment', pango
.ALIGN_RIGHT
)
893 countcell
.set_property('xalign', 1.)
894 countcell
.set_property('xpad', 5)
895 namecolumn
.pack_start(countcell
, False)
896 namecolumn
.add_attribute(countcell
, 'text', PodcastListModel
.C_DOWNLOADS
)
897 namecolumn
.add_attribute(countcell
, 'visible', PodcastListModel
.C_DOWNLOADS
)
899 iconcell
= gtk
.CellRendererPixbuf()
900 iconcell
.set_property('xalign', 1.0)
901 namecolumn
.pack_start(iconcell
, False)
902 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
903 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
905 self
.treeChannels
.append_column(namecolumn
)
907 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
909 # When no podcast is selected, clear the episode list model
910 selection
= self
.treeChannels
.get_selection()
911 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
913 # Set up type-ahead find for the podcast list
914 def on_key_press(treeview
, event
):
915 if gpodder
.ui
.desktop
and event
.keyval
== gtk
.keysyms
.Right
:
916 self
.treeAvailable
.grab_focus()
917 elif event
.keyval
== gtk
.keysyms
.Escape
:
918 self
.hide_podcast_search()
919 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
920 self
.hide_podcast_search()
921 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
922 # Don't handle type-ahead when control is pressed (so shortcuts
923 # with the Ctrl key still work, e.g. Ctrl+A, ...)
926 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
927 if unicode_char_id
== 0:
929 input_char
= unichr(unicode_char_id
)
930 self
.show_podcast_search(input_char
)
932 self
.treeChannels
.connect('key-press-event', on_key_press
)
934 self
.treeChannels
.connect('popup-menu', self
.treeview_channels_show_context_menu
)
936 # Enable separators to the podcast list to separate special podcasts
937 # from others (this is used for the "all episodes" view)
938 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
940 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
942 def on_entry_search_episodes_changed(self
, editable
):
943 if self
.hbox_search_episodes
.get_property('visible'):
944 def set_search_term(self
, text
):
945 self
.episode_list_model
.set_search_term(text
)
946 self
._episode
_list
_search
_timeout
= None
949 if self
._episode
_list
_search
_timeout
is not None:
950 gobject
.source_remove(self
._episode
_list
_search
_timeout
)
951 self
._episode
_list
_search
_timeout
= gobject
.timeout_add(\
952 self
.LIVE_SEARCH_DELAY
, \
953 set_search_term
, self
, editable
.get_chars(0, -1))
955 def on_entry_search_episodes_key_press(self
, editable
, event
):
956 if event
.keyval
== gtk
.keysyms
.Escape
:
957 self
.hide_episode_search()
960 def hide_episode_search(self
, *args
):
961 if self
._episode
_list
_search
_timeout
is not None:
962 gobject
.source_remove(self
._episode
_list
_search
_timeout
)
963 self
._episode
_list
_search
_timeout
= None
964 self
.hbox_search_episodes
.hide()
965 self
.entry_search_episodes
.set_text('')
966 self
.episode_list_model
.set_search_term(None)
967 self
.treeAvailable
.grab_focus()
969 def show_episode_search(self
, input_char
):
970 self
.hbox_search_episodes
.show()
971 self
.entry_search_episodes
.insert_text(input_char
, -1)
972 self
.entry_search_episodes
.grab_focus()
973 self
.entry_search_episodes
.set_position(-1)
975 def set_episode_list_column(self
, index
, new_value
):
978 self
.config
.episode_list_columns |
= mask
980 self
.config
.episode_list_columns
&= ~mask
982 def update_episode_list_columns_visibility(self
):
983 if gpodder
.ui
.fremantle
:
986 columns
= TreeViewHelper
.get_columns(self
.treeAvailable
)
987 for index
, column
in enumerate(columns
):
988 visible
= bool(self
.config
.episode_list_columns
& (1 << index
))
989 column
.set_visible(visible
)
990 self
.treeAvailable
.columns_autosize()
992 if self
.episode_columns_menu
is not None:
993 children
= self
.episode_columns_menu
.get_children()
994 for index
, child
in enumerate(children
):
995 active
= bool(self
.config
.episode_list_columns
& (1 << index
))
996 child
.set_active(active
)
998 def on_episode_list_header_clicked(self
, button
, event
):
999 if event
.button
!= 3:
1002 if self
.episode_columns_menu
is not None:
1003 self
.episode_columns_menu
.popup(None, None, None, event
.button
, \
1008 def init_episode_list_treeview(self
):
1009 # For loading the list model
1010 self
.episode_list_model
= EpisodeListModel(self
.on_episode_list_filter_changed
)
1012 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
1013 self
.item_view_episodes_undeleted
.set_active(True)
1014 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
1015 self
.item_view_episodes_downloaded
.set_active(True)
1016 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
1017 self
.item_view_episodes_unplayed
.set_active(True)
1019 self
.item_view_episodes_all
.set_active(True)
1021 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
1023 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
1025 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
1027 iconcell
= gtk
.CellRendererPixbuf()
1028 iconcell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1029 if gpodder
.ui
.fremantle
:
1030 iconcell
.set_fixed_size(50, 50)
1032 iconcell
.set_fixed_size(40, -1)
1034 namecell
= gtk
.CellRendererText()
1035 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1036 namecolumn
= gtk
.TreeViewColumn(_('Episode'))
1037 namecolumn
.pack_start(iconcell
, False)
1038 namecolumn
.add_attribute(iconcell
, 'icon-name', EpisodeListModel
.C_STATUS_ICON
)
1039 namecolumn
.pack_start(namecell
, True)
1040 namecolumn
.add_attribute(namecell
, 'markup', EpisodeListModel
.C_DESCRIPTION
)
1041 if gpodder
.ui
.fremantle
:
1042 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_FIXED
)
1044 namecolumn
.set_sort_column_id(EpisodeListModel
.C_DESCRIPTION
)
1045 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1046 namecolumn
.set_resizable(True)
1047 namecolumn
.set_expand(True)
1049 if gpodder
.ui
.fremantle
:
1050 from gpodder
.gtkui
.frmntl
import style
1051 timecell
= gtk
.CellRendererText()
1052 timecell
.set_property('font-desc', style
.get_font_desc('SmallSystemFont'))
1053 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1054 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1055 timecell
.set_property('xalign', 1.)
1056 timecell
.set_property('xpad', 5)
1057 timecell
.set_property('yalign', .85)
1058 namecolumn
.pack_start(timecell
, False)
1059 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1060 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME_VISIBLE
)
1062 lockcell
= gtk
.CellRendererPixbuf()
1063 lockcell
.set_fixed_size(40, -1)
1064 lockcell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1065 lockcell
.set_property('icon-name', 'emblem-readonly')
1066 namecolumn
.pack_start(lockcell
, False)
1067 namecolumn
.add_attribute(lockcell
, 'visible', EpisodeListModel
.C_LOCKED
)
1069 sizecell
= gtk
.CellRendererText()
1070 sizecell
.set_property('xalign', 1)
1071 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
1072 sizecolumn
.set_sort_column_id(EpisodeListModel
.C_FILESIZE
)
1074 timecell
= gtk
.CellRendererText()
1075 timecell
.set_property('xalign', 1)
1076 timecolumn
= gtk
.TreeViewColumn(_('Duration'), timecell
, text
=EpisodeListModel
.C_TIME
)
1077 timecolumn
.set_sort_column_id(EpisodeListModel
.C_TOTAL_TIME
)
1079 releasecell
= gtk
.CellRendererText()
1080 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
1081 releasecolumn
.set_sort_column_id(EpisodeListModel
.C_PUBLISHED
)
1083 namecolumn
.set_reorderable(True)
1084 self
.treeAvailable
.append_column(namecolumn
)
1086 if gpodder
.ui
.desktop
:
1087 for itemcolumn
in (sizecolumn
, timecolumn
, releasecolumn
):
1088 itemcolumn
.set_reorderable(True)
1089 self
.treeAvailable
.append_column(itemcolumn
)
1090 TreeViewHelper
.register_column(self
.treeAvailable
, itemcolumn
)
1092 # Add context menu to all tree view column headers
1093 for column
in self
.treeAvailable
.get_columns():
1094 label
= gtk
.Label(column
.get_title())
1096 column
.set_widget(label
)
1098 w
= column
.get_widget()
1099 while w
is not None and not isinstance(w
, gtk
.Button
):
1102 w
.connect('button-release-event', self
.on_episode_list_header_clicked
)
1104 # Create a new menu for the visible episode list columns
1105 for child
in self
.mainMenu
.get_children():
1106 if child
.get_name() == 'menuView':
1107 submenu
= child
.get_submenu()
1108 item
= gtk
.MenuItem(_('Visible columns'))
1109 submenu
.append(gtk
.SeparatorMenuItem())
1110 submenu
.append(item
)
1113 self
.episode_columns_menu
= gtk
.Menu()
1114 item
.set_submenu(self
.episode_columns_menu
)
1117 # For each column that can be shown/hidden, add a menu item
1118 columns
= TreeViewHelper
.get_columns(self
.treeAvailable
)
1119 for index
, column
in enumerate(columns
):
1120 item
= gtk
.CheckMenuItem(column
.get_title())
1121 self
.episode_columns_menu
.append(item
)
1122 def on_item_toggled(item
, index
):
1123 self
.set_episode_list_column(index
, item
.get_active())
1124 item
.connect('toggled', on_item_toggled
, index
)
1125 self
.episode_columns_menu
.show_all()
1127 # Update the visibility of the columns and the check menu items
1128 self
.update_episode_list_columns_visibility()
1130 # Set up type-ahead find for the episode list
1131 def on_key_press(treeview
, event
):
1132 if gpodder
.ui
.desktop
and event
.keyval
== gtk
.keysyms
.Left
:
1133 self
.treeChannels
.grab_focus()
1134 elif event
.keyval
== gtk
.keysyms
.Escape
:
1135 self
.hide_episode_search()
1136 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
1137 self
.hide_episode_search()
1138 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
1139 # Don't handle type-ahead when control is pressed (so shortcuts
1140 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1143 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1144 if unicode_char_id
== 0:
1146 input_char
= unichr(unicode_char_id
)
1147 self
.show_episode_search(input_char
)
1149 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1151 self
.treeAvailable
.connect('popup-menu', self
.treeview_available_show_context_menu
)
1153 if gpodder
.ui
.desktop
:
1154 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1155 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1156 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1157 uris
= ['file://'+e
.local_filename(create
=False) \
1158 for e
in self
.get_selected_episodes() \
1159 if e
.was_downloaded(and_exists
=True)]
1160 uris
.append('') # for the trailing '\r\n'
1161 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1162 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1164 selection
= self
.treeAvailable
.get_selection()
1165 if gpodder
.ui
.fremantle
:
1166 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1168 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1169 # Update the sensitivity of the toolbar buttons on the Desktop
1170 selection
.connect('changed', lambda s
: self
.play_or_download())
1172 def init_download_list_treeview(self
):
1173 # enable multiple selection support
1174 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1175 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1177 # columns and renderers for "download progress" tab
1178 # First column: [ICON] Episodename
1179 column
= gtk
.TreeViewColumn(_('Episode'))
1181 cell
= gtk
.CellRendererPixbuf()
1182 if gpodder
.ui
.fremantle
:
1183 cell
.set_fixed_size(50, 50)
1184 cell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1185 column
.pack_start(cell
, expand
=False)
1186 column
.add_attribute(cell
, 'icon-name', \
1187 DownloadStatusModel
.C_ICON_NAME
)
1189 cell
= gtk
.CellRendererText()
1190 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1191 column
.pack_start(cell
, expand
=True)
1192 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1193 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1194 column
.set_expand(True)
1195 self
.treeDownloads
.append_column(column
)
1197 # Second column: Progress
1198 cell
= gtk
.CellRendererProgress()
1199 cell
.set_property('yalign', .5)
1200 cell
.set_property('ypad', 6)
1201 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1202 value
=DownloadStatusModel
.C_PROGRESS
, \
1203 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1204 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1205 column
.set_expand(False)
1206 self
.treeDownloads
.append_column(column
)
1207 if gpodder
.ui
.fremantle
:
1208 column
.set_property('min-width', 200)
1209 column
.set_property('max-width', 200)
1211 column
.set_property('min-width', 150)
1212 column
.set_property('max-width', 150)
1214 self
.treeDownloads
.set_model(self
.download_status_model
)
1215 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1217 self
.treeDownloads
.connect('popup-menu', self
.treeview_downloads_show_context_menu
)
1219 def on_treeview_expose_event(self
, treeview
, event
):
1220 if event
.window
== treeview
.get_bin_window():
1221 model
= treeview
.get_model()
1222 if (model
is not None and model
.get_iter_first() is not None):
1225 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1229 ctx
= event
.window
.cairo_create()
1230 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1231 event
.area
.width
, event
.area
.height
)
1234 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1237 if role
== TreeViewHelper
.ROLE_EPISODES
:
1238 if self
.currently_updating
:
1239 text
= _('Loading episodes')
1240 elif self
.config
.episode_list_view_mode
!= \
1241 EpisodeListModel
.VIEW_ALL
:
1242 text
= _('No episodes in current view')
1244 text
= _('No episodes available')
1245 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1246 if self
.config
.episode_list_view_mode
!= \
1247 EpisodeListModel
.VIEW_ALL
and \
1248 self
.config
.podcast_list_hide_boring
and \
1249 len(self
.channels
) > 0:
1250 text
= _('No podcasts in this view')
1252 text
= _('No subscriptions')
1253 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1254 text
= _('No active downloads')
1256 raise Exception('on_treeview_expose_event: unknown role')
1258 if gpodder
.ui
.fremantle
:
1259 from gpodder
.gtkui
.frmntl
import style
1260 font_desc
= style
.get_font_desc('LargeSystemFont')
1264 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1268 def enable_download_list_update(self
):
1269 if not self
.download_list_update_enabled
:
1270 self
.update_downloads_list()
1271 gobject
.timeout_add(1500, self
.update_downloads_list
)
1272 self
.download_list_update_enabled
= True
1274 def cleanup_downloads(self
):
1275 model
= self
.download_status_model
1277 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1278 changed_episode_urls
= set()
1279 for row_reference
, task
in all_tasks
:
1280 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1281 model
.remove(model
.get_iter(row_reference
.get_path()))
1283 # We don't "see" this task anymore - remove it;
1284 # this is needed, so update_episode_list_icons()
1285 # below gets the correct list of "seen" tasks
1286 self
.download_tasks_seen
.remove(task
)
1287 except KeyError, key_error
:
1288 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1289 changed_episode_urls
.add(task
.url
)
1290 # Tell the task that it has been removed (so it can clean up)
1291 task
.removed_from_list()
1293 # Tell the podcasts tab to update icons for our removed podcasts
1294 self
.update_episode_list_icons(changed_episode_urls
)
1296 # Tell the shownotes window that we have removed the episode
1297 if self
.episode_shownotes_window
is not None and \
1298 self
.episode_shownotes_window
.episode
is not None and \
1299 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1300 self
.episode_shownotes_window
._download
_status
_changed
(None)
1302 # Update the downloads list one more time
1303 self
.update_downloads_list(can_call_cleanup
=False)
1305 def on_tool_downloads_toggled(self
, toolbutton
):
1306 if toolbutton
.get_active():
1307 self
.wNotebook
.set_current_page(1)
1309 self
.wNotebook
.set_current_page(0)
1311 def add_download_task_monitor(self
, monitor
):
1312 self
.download_task_monitors
.add(monitor
)
1313 model
= self
.download_status_model
1317 task
= row
[self
.download_status_model
.C_TASK
]
1318 monitor
.task_updated(task
)
1320 def remove_download_task_monitor(self
, monitor
):
1321 self
.download_task_monitors
.remove(monitor
)
1323 def update_downloads_list(self
, can_call_cleanup
=True):
1325 model
= self
.download_status_model
1327 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1328 total_speed
, total_size
, done_size
= 0, 0, 0
1330 # Keep a list of all download tasks that we've seen
1331 download_tasks_seen
= set()
1333 # Remember the DownloadTask object for the episode that
1334 # has been opened in the episode shownotes dialog (if any)
1335 if self
.episode_shownotes_window
is not None:
1336 shownotes_episode
= self
.episode_shownotes_window
.episode
1337 shownotes_task
= None
1339 shownotes_episode
= None
1340 shownotes_task
= None
1342 # Do not go through the list of the model is not (yet) available
1347 self
.download_status_model
.request_update(row
.iter)
1349 task
= row
[self
.download_status_model
.C_TASK
]
1350 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1352 # Let the download task monitors know of changes
1353 for monitor
in self
.download_task_monitors
:
1354 monitor
.task_updated(task
)
1357 done_size
+= size
*progress
1359 if shownotes_episode
is not None and \
1360 shownotes_episode
.url
== task
.episode
.url
:
1361 shownotes_task
= task
1363 download_tasks_seen
.add(task
)
1365 if status
== download
.DownloadTask
.DOWNLOADING
:
1367 total_speed
+= speed
1368 elif status
== download
.DownloadTask
.FAILED
:
1370 elif status
== download
.DownloadTask
.DONE
:
1372 elif status
== download
.DownloadTask
.QUEUED
:
1374 elif status
== download
.DownloadTask
.PAUSED
:
1379 # Remember which tasks we have seen after this run
1380 self
.download_tasks_seen
= download_tasks_seen
1382 if gpodder
.ui
.desktop
:
1383 text
= [_('Downloads')]
1384 if downloading
+ failed
+ queued
> 0:
1387 s
.append(N_('%(count)d active', '%(count)d active', downloading
) % {'count':downloading
})
1389 s
.append(N_('%(count)d failed', '%(count)d failed', failed
) % {'count':failed
})
1391 s
.append(N_('%(count)d queued', '%(count)d queued', queued
) % {'count':queued
})
1392 text
.append(' (' + ', '.join(s
)+')')
1393 self
.labelDownloads
.set_text(''.join(text
))
1394 if gpodder
.ui
.fremantle
:
1395 if downloading
+ queued
> 0:
1396 self
.button_downloads
.set_value(N_('%(count)d active', '%(count)d active', downloading
+queued
) % {'count':(downloading
+queued
)})
1398 self
.button_downloads
.set_value(N_('%(count)d failed', '%(count)d failed', failed
) % {'count':failed
})
1400 self
.button_downloads
.set_value(N_('%(count)d paused', '%(count)d paused', paused
) % {'count':paused
})
1402 self
.button_downloads
.set_value(_('Idle'))
1404 title
= [self
.default_title
]
1406 # We have to update all episodes/channels for which the status has
1407 # changed. Accessing task.status_changed has the side effect of
1408 # re-setting the changed flag, so we need to get the "changed" list
1409 # of tuples first and split it into two lists afterwards
1410 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1411 self
.download_tasks_seen
if task
.status_changed
]
1412 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1413 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1415 count
= downloading
+ queued
1417 title
.append(N_('downloading %(count)d file', 'downloading %(count)d files', count
) % {'count':count
})
1420 percentage
= 100.0*done_size
/total_size
1423 total_speed
= util
.format_filesize(total_speed
)
1424 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1426 if gpodder
.ui
.desktop
:
1427 self
.downloads_finished(self
.download_tasks_seen
)
1428 log('All downloads have finished.', sender
=self
)
1430 if gpodder
.ui
.fremantle
:
1431 message
= '\n'.join(['%s: %s' % (str(task
), \
1432 task
.error_message
) for task
in self
.download_tasks_seen
if task
.notify_as_failed()])
1434 self
.show_message(message
, _('Downloads failed'), important
=True)
1436 # Remove finished episodes
1437 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1438 self
.cleanup_downloads()
1440 # Stop updating the download list here
1441 self
.download_list_update_enabled
= False
1443 if not gpodder
.ui
.fremantle
:
1444 self
.gPodder
.set_title(' - '.join(title
))
1446 self
.update_episode_list_icons(episode_urls
)
1447 if self
.episode_shownotes_window
is not None:
1448 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1449 shownotes_task
!= self
.episode_shownotes_window
.task
:
1450 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1451 self
.episode_shownotes_window
._download
_status
_progress
()
1452 self
.play_or_download()
1454 self
.update_podcast_list_model(channel_urls
)
1456 return self
.download_list_update_enabled
1457 except Exception, e
:
1458 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1459 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1460 # We return False here, so the update loop won't be called again,
1461 # that's why we require the restart of gPodder in the message.
1464 def on_config_changed(self
, *args
):
1465 util
.idle_add(self
._on
_config
_changed
, *args
)
1467 def _on_config_changed(self
, name
, old_value
, new_value
):
1468 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1469 self
.toolbar
.set_property('visible', new_value
)
1470 elif name
== 'episode_list_descriptions':
1471 self
.update_episode_list_model()
1472 elif name
== 'rotation_mode':
1473 self
._fremantle
_rotation
.set_mode(new_value
)
1474 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1475 self
.restart_auto_update_timer()
1476 elif name
== 'podcast_list_view_all':
1477 # Force a update of the podcast list model
1478 self
.channel_list_changed
= True
1479 if gpodder
.ui
.fremantle
:
1480 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1481 while gtk
.events_pending():
1482 gtk
.main_iteration(False)
1483 self
.update_podcast_list_model()
1484 if gpodder
.ui
.fremantle
:
1485 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1486 elif name
== 'episode_list_columns':
1487 self
.update_episode_list_columns_visibility()
1488 elif name
== '_paned_position':
1489 self
._last
_paned
_position
_toggling
= True
1490 self
.item_sidebar
.set_active(new_value
> 0)
1491 self
._last
_paned
_position
_toggling
= False
1493 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1494 # With get_bin_window, we get the window that contains the rows without
1495 # the header. The Y coordinate of this window will be the height of the
1496 # treeview header. This is the amount we have to subtract from the
1497 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1498 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1501 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1503 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or x
> 50 or (column
is not None and column
!= treeview
.get_columns()[0]):
1504 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1507 if path
is not None:
1508 model
= treeview
.get_model()
1509 iter = model
.get_iter(path
)
1510 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1512 if role
== TreeViewHelper
.ROLE_EPISODES
:
1513 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1514 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1515 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1517 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1518 if last_tooltip
is not None and last_tooltip
!= id:
1519 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1521 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1523 if role
== TreeViewHelper
.ROLE_EPISODES
:
1524 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1526 tooltip
.set_text(description
)
1529 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1530 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1533 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1535 error_str
= _('Feedparser error: %s') % cgi
.escape(error_str
.strip())
1536 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1537 table
= gtk
.Table(rows
=3, columns
=3)
1538 table
.set_row_spacings(5)
1539 table
.set_col_spacings(5)
1540 table
.set_border_width(5)
1542 heading
= gtk
.Label()
1543 heading
.set_alignment(0, 1)
1544 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (cgi
.escape(channel
.title
), cgi
.escape(channel
.url
)))
1545 table
.attach(heading
, 0, 1, 0, 1)
1547 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1549 if len(channel
.description
) < 500:
1550 description
= channel
.description
1552 pos
= channel
.description
.find('\n\n')
1553 if pos
== -1 or pos
> 500:
1554 description
= channel
.description
[:498]+'[...]'
1556 description
= channel
.description
[:pos
]
1558 description
= gtk
.Label(description
)
1560 description
.set_markup(error_str
)
1561 description
.set_alignment(0, 0)
1562 description
.set_line_wrap(True)
1563 table
.attach(description
, 0, 3, 2, 3)
1566 tooltip
.set_custom(table
)
1570 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1573 def treeview_allow_tooltips(self
, treeview
, allow
):
1574 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1576 def treeview_handle_context_menu_click(self
, treeview
, event
):
1578 selection
= treeview
.get_selection()
1579 return selection
.get_selected_rows()
1581 x
, y
= int(event
.x
), int(event
.y
)
1582 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1584 selection
= treeview
.get_selection()
1585 model
, paths
= selection
.get_selected_rows()
1587 if path
is None or (path
not in paths
and \
1588 event
.button
== self
.context_menu_mouse_button
):
1589 # We have right-clicked, but not into the selection,
1590 # assume we don't want to operate on the selection
1593 if path
is not None and not paths
and \
1594 event
.button
== self
.context_menu_mouse_button
:
1595 # No selection or clicked outside selection;
1596 # select the single item where we clicked
1597 treeview
.grab_focus()
1598 treeview
.set_cursor(path
, column
, 0)
1602 # Unselect any remaining items (clicked elsewhere)
1603 if hasattr(treeview
, 'is_rubber_banding_active'):
1604 if not treeview
.is_rubber_banding_active():
1605 selection
.unselect_all()
1607 selection
.unselect_all()
1611 def downloads_list_get_selection(self
, model
=None, paths
=None):
1612 if model
is None and paths
is None:
1613 selection
= self
.treeDownloads
.get_selection()
1614 model
, paths
= selection
.get_selected_rows()
1616 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1617 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1618 model
.get_value(model
.get_iter(path
), \
1619 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1621 for row_reference
, task
in selected_tasks
:
1622 if task
.status
!= download
.DownloadTask
.QUEUED
:
1624 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1625 download
.DownloadTask
.FAILED
, \
1626 download
.DownloadTask
.CANCELLED
):
1628 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1629 download
.DownloadTask
.QUEUED
, \
1630 download
.DownloadTask
.DOWNLOADING
, \
1631 download
.DownloadTask
.FAILED
):
1633 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1634 download
.DownloadTask
.DOWNLOADING
):
1636 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1637 download
.DownloadTask
.FAILED
, \
1638 download
.DownloadTask
.DONE
):
1641 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1643 def downloads_finished(self
, download_tasks_seen
):
1644 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.notify_as_finished()]
1645 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.notify_as_failed()]
1647 if finished_downloads
and failed_downloads
:
1648 message
= self
.format_episode_list(finished_downloads
, 5)
1649 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1650 message
+= self
.format_episode_list(failed_downloads
, 5)
1651 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1652 elif finished_downloads
:
1653 message
= self
.format_episode_list(finished_downloads
)
1654 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1655 elif failed_downloads
:
1656 message
= self
.format_episode_list(failed_downloads
)
1657 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1660 def format_episode_list(self
, episode_list
, max_episodes
=10):
1662 Format a list of episode names for notifications
1664 Will truncate long episode names and limit the amount of
1665 episodes displayed (max_episodes=10).
1667 The episode_list parameter should be a list of strings.
1669 MAX_TITLE_LENGTH
= 100
1672 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1673 if len(title
) > MAX_TITLE_LENGTH
:
1674 middle
= (MAX_TITLE_LENGTH
/2)-2
1675 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1676 result
.append(cgi
.escape(title
))
1679 more_episodes
= len(episode_list
) - max_episodes
1680 if more_episodes
> 0:
1681 result
.append('(...')
1682 result
.append(N_('%(count)d more episode', '%(count)d more episodes', more_episodes
) % {'count':more_episodes
})
1683 result
.append('...)')
1685 return (''.join(result
)).strip()
1687 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1688 episode_urls
= set()
1689 model
= self
.treeDownloads
.get_model()
1690 for row_reference
, task
in tasks
:
1691 if status
== download
.DownloadTask
.QUEUED
:
1692 # Only queue task when its paused/failed/cancelled (or forced)
1693 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1694 self
.download_queue_manager
.add_task(task
, force_start
)
1695 self
.enable_download_list_update()
1696 elif status
== download
.DownloadTask
.CANCELLED
:
1697 # Cancelling a download allowed when downloading/queued
1698 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1699 task
.status
= status
1700 # Cancelling paused/failed downloads requires a call to .run()
1701 elif task
.status
in (task
.PAUSED
, task
.FAILED
):
1702 task
.status
= status
1703 # Call run, so the partial file gets deleted
1705 elif status
== download
.DownloadTask
.PAUSED
:
1706 # Pausing a download only when queued/downloading
1707 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1708 task
.status
= status
1709 elif status
is None:
1710 # Remove the selected task - cancel downloading/queued tasks
1711 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1712 task
.status
= task
.CANCELLED
1713 model
.remove(model
.get_iter(row_reference
.get_path()))
1714 # Remember the URL, so we can tell the UI to update
1716 # We don't "see" this task anymore - remove it;
1717 # this is needed, so update_episode_list_icons()
1718 # below gets the correct list of "seen" tasks
1719 self
.download_tasks_seen
.remove(task
)
1720 except KeyError, key_error
:
1721 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1722 episode_urls
.add(task
.url
)
1723 # Tell the task that it has been removed (so it can clean up)
1724 task
.removed_from_list()
1726 # We can (hopefully) simply set the task status here
1727 task
.status
= status
1728 # Tell the podcasts tab to update icons for our removed podcasts
1729 self
.update_episode_list_icons(episode_urls
)
1730 # Update the tab title and downloads list
1731 self
.update_downloads_list()
1733 def treeview_downloads_show_context_menu(self
, treeview
, event
=None):
1734 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1736 if not hasattr(treeview
, 'is_rubber_banding_active'):
1739 return not treeview
.is_rubber_banding_active()
1741 if event
is None or event
.button
== self
.context_menu_mouse_button
:
1742 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1743 self
.downloads_list_get_selection(model
, paths
)
1745 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1746 # This creates a menu item for selection-wide actions
1747 item
= gtk
.ImageMenuItem(label
)
1748 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1749 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1750 item
.set_sensitive(sensitive
)
1755 item
= gtk
.ImageMenuItem(_('Episode details'))
1756 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1757 if len(selected_tasks
) == 1:
1758 row_reference
, task
= selected_tasks
[0]
1759 episode
= task
.episode
1760 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1762 item
.set_sensitive(False)
1764 menu
.append(gtk
.SeparatorMenuItem())
1766 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1768 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1769 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1770 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1771 menu
.append(gtk
.SeparatorMenuItem())
1772 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1777 func
= TreeViewHelper
.make_popup_position_func(treeview
)
1778 menu
.popup(None, None, func
, self
.context_menu_mouse_button
, 0)
1780 menu
.popup(None, None, None, event
.button
, event
.time
)
1783 def treeview_channels_show_context_menu(self
, treeview
, event
=None):
1784 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1788 # Check for valid channel id, if there's no id then
1789 # assume that it is a proxy channel or equivalent
1790 # and cannot be operated with right click
1791 if self
.active_channel
.id is None:
1794 if event
is None or event
.button
== self
.context_menu_mouse_button
:
1799 item
= gtk
.ImageMenuItem( _('Update podcast'))
1800 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1801 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1802 item
.set_sensitive(not self
.updating_feed_cache
)
1805 menu
.append(gtk
.SeparatorMenuItem())
1807 item
= gtk
.CheckMenuItem(_('Archive'))
1808 item
.set_active(self
.active_channel
.auto_archive_episodes
)
1809 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1812 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1813 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1814 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1817 menu
.append( gtk
.SeparatorMenuItem())
1819 item
= gtk
.ImageMenuItem(_('Podcast details'))
1820 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1821 item
.connect('activate', self
.on_itemEditChannel_activate
)
1825 # Disable tooltips while we are showing the menu, so
1826 # the tooltip will not appear over the menu
1827 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1828 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1831 func
= TreeViewHelper
.make_popup_position_func(treeview
)
1832 menu
.popup(None, None, func
, self
.context_menu_mouse_button
, 0)
1834 menu
.popup(None, None, None, event
.button
, event
.time
)
1838 def cover_file_removed(self
, channel_url
):
1840 The Cover Downloader calls this when a previously-
1841 available cover has been removed from the disk. We
1842 have to update our model to reflect this change.
1844 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1846 def cover_download_finished(self
, channel
, pixbuf
):
1848 The Cover Downloader calls this when it has finished
1849 downloading (or registering, if already downloaded)
1850 a new channel cover, which is ready for displaying.
1852 self
.podcast_list_model
.add_cover_by_channel(channel
, pixbuf
)
1854 def save_episodes_as_file(self
, episodes
):
1855 for episode
in episodes
:
1856 self
.save_episode_as_file(episode
)
1858 def save_episode_as_file(self
, episode
):
1859 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1860 if episode
.was_downloaded(and_exists
=True):
1861 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1862 copy_from
= episode
.local_filename(create
=False)
1863 assert copy_from
is not None
1864 copy_to
= util
.sanitize_filename(episode
.sync_filename())
1865 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1866 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1868 def copy_episodes_bluetooth(self
, episodes
):
1869 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1871 def convert_and_send_thread(episode
):
1872 for episode
in episodes
:
1873 filename
= episode
.local_filename(create
=False)
1874 assert filename
is not None
1875 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1876 util
.sanitize_filename(episode
.sync_filename()))
1877 (base
, ext
) = os
.path
.splitext(filename
)
1878 if not destfile
.endswith(ext
):
1882 shutil
.copyfile(filename
, destfile
)
1883 util
.bluetooth_send_file(destfile
)
1885 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1886 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1888 util
.delete_file(destfile
)
1890 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1892 def _treeview_button_released(self
, treeview
, event
):
1893 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1894 dy
= int(abs(event
.y
-ypos
))
1895 dx
= int(event
.x
-xpos
)
1897 selection
= treeview
.get_selection()
1898 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1899 if path
is None or dy
> 30:
1900 return (False, dx
, dy
)
1902 path
, column
, x
, y
= path
1903 selection
.select_path(path
)
1904 treeview
.set_cursor(path
)
1905 treeview
.grab_focus()
1907 return (True, dx
, dy
)
1909 def treeview_available_show_context_menu(self
, treeview
, event
=None):
1910 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1912 if not hasattr(treeview
, 'is_rubber_banding_active'):
1915 return not treeview
.is_rubber_banding_active()
1917 if event
is None or event
.button
== self
.context_menu_mouse_button
:
1918 episodes
= self
.get_selected_episodes()
1919 any_locked
= any(e
.archive
for e
in episodes
)
1920 any_played
= any(not e
.is_new
for e
in episodes
)
1921 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and e
.is_new
for e
in episodes
)
1922 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1923 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
1927 (can_play
, can_download
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1929 if open_instead_of_play
:
1930 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1932 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1934 item
= gtk
.ImageMenuItem(_('Stream'))
1935 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
1937 item
.set_sensitive(can_play
and not downloading
)
1938 item
.connect('activate', self
.on_playback_selected_episodes
)
1942 item
= gtk
.ImageMenuItem(_('Download'))
1943 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1944 item
.set_sensitive(can_download
)
1945 item
.connect('activate', self
.on_download_selected_episodes
)
1948 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1949 item
.connect('activate', self
.on_item_cancel_download_activate
)
1952 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1953 item
.set_sensitive(can_delete
)
1954 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1957 result
= gpodder
.user_hooks
.on_episodes_context_menu(episodes
)
1959 menu
.append(gtk
.SeparatorMenuItem())
1960 for label
, callback
in result
:
1961 item
= gtk
.MenuItem(label
)
1962 item
.connect('activate', lambda item
: callback(episodes
))
1967 # Ok, this probably makes sense to only display for downloaded files
1969 menu
.append(gtk
.SeparatorMenuItem())
1970 share_item
= gtk
.MenuItem(_('Send to'))
1971 menu
.append(share_item
)
1972 share_menu
= gtk
.Menu()
1974 item
= gtk
.ImageMenuItem(_('Local folder'))
1975 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
1976 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
1977 share_menu
.append(item
)
1978 if self
.bluetooth_available
:
1979 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
1980 item
.set_image(gtk
.image_new_from_icon_name(ICON('bluetooth'), gtk
.ICON_SIZE_MENU
))
1981 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
1982 share_menu
.append(item
)
1984 share_item
.set_submenu(share_menu
)
1986 if (downloaded
or one_is_new
or can_download
) and not downloading
:
1987 menu
.append(gtk
.SeparatorMenuItem())
1989 item
= gtk
.CheckMenuItem(_('New'))
1990 item
.set_active(True)
1991 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
1994 item
= gtk
.CheckMenuItem(_('New'))
1995 item
.set_active(False)
1996 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
2000 item
= gtk
.CheckMenuItem(_('Played'))
2001 item
.set_active(any_played
)
2002 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
2005 item
= gtk
.CheckMenuItem(_('Archive'))
2006 item
.set_active(any_locked
)
2007 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
2010 menu
.append(gtk
.SeparatorMenuItem())
2011 # Single item, add episode information menu item
2012 item
= gtk
.ImageMenuItem(_('Episode details'))
2013 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
2014 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
2018 # Disable tooltips while we are showing the menu, so
2019 # the tooltip will not appear over the menu
2020 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
2021 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2023 func
= TreeViewHelper
.make_popup_position_func(treeview
)
2024 menu
.popup(None, None, func
, self
.context_menu_mouse_button
, 0)
2026 menu
.popup(None, None, None, event
.button
, event
.time
)
2030 def set_title(self
, new_title
):
2031 if not gpodder
.ui
.fremantle
:
2032 self
.default_title
= new_title
2033 self
.gPodder
.set_title(new_title
)
2035 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2037 Updates the status icons in the episode list.
2039 If urls is given, it should be a list of URLs
2040 of episodes that should be updated.
2042 If urls is None, set ONE OF selected, all to
2043 True (the former updates just the selected
2044 episodes and the latter updates all episodes).
2046 additional_args
= (self
.episode_is_downloading
, \
2047 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
)
2049 if urls
is not None:
2050 # We have a list of URLs to walk through
2051 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2052 elif selected
and not all
:
2053 # We should update all selected episodes
2054 selection
= self
.treeAvailable
.get_selection()
2055 model
, paths
= selection
.get_selected_rows()
2056 for path
in reversed(paths
):
2057 iter = model
.get_iter(path
)
2058 self
.episode_list_model
.update_by_filter_iter(iter, \
2060 elif all
and not selected
:
2061 # We update all (even the filter-hidden) episodes
2062 self
.episode_list_model
.update_all(*additional_args
)
2064 # Wrong/invalid call - have to specify at least one parameter
2065 raise ValueError('Invalid call to update_episode_list_icons')
2067 def episode_list_status_changed(self
, episodes
):
2068 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2069 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2072 def clean_up_downloads(self
, delete_partial
=False):
2073 # Clean up temporary files left behind by old gPodder versions
2074 temporary_files
= glob
.glob('%s/*/.tmp-*' % gpodder
.downloads
)
2077 temporary_files
+= glob
.glob('%s/*/*.partial' % gpodder
.downloads
)
2079 for tempfile
in temporary_files
:
2080 util
.delete_file(tempfile
)
2083 def streaming_possible(self
):
2084 if gpodder
.ui
.desktop
:
2085 # User has to have a media player set on the Desktop, or else we
2086 # would probably open the browser when giving a URL to xdg-open..
2087 return (self
.config
.player
and self
.config
.player
!= 'default')
2088 elif gpodder
.ui
.fremantle
:
2089 # On Maemo, the default is to use the Nokia Media Player, which is
2090 # already able to deal with HTTP URLs the right way, so we
2091 # unconditionally enable streaming always on Maemo
2096 def playback_episodes_for_real(self
, episodes
):
2097 groups
= collections
.defaultdict(list)
2098 for episode
in episodes
:
2099 file_type
= episode
.file_type()
2100 if file_type
== 'video' and self
.config
.videoplayer
and \
2101 self
.config
.videoplayer
!= 'default':
2102 player
= self
.config
.videoplayer
2103 if gpodder
.ui
.fremantle
and player
== 'mplayer':
2104 player
= 'mplayer -fs %F'
2105 elif file_type
== 'audio' and self
.config
.player
and \
2106 self
.config
.player
!= 'default':
2107 player
= self
.config
.player
2111 # Mark episode as played in the database
2112 episode
.playback_mark()
2113 self
.mygpo_client
.on_playback([episode
])
2115 fmt_id
= self
.config
.youtube_preferred_fmt_id
2116 filename
= episode
.get_playback_url(fmt_id
)
2118 # Determine the playback resume position - if the file
2119 # was played 100%, we simply start from the beginning
2120 resume_position
= episode
.current_position
2121 if resume_position
== episode
.total_time
:
2124 # Only on Maemo 5, and only if the episode isn't finished yet
2125 if gpodder
.ui
.fremantle
and not episode
.is_finished():
2126 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2128 # If Panucci is configured, use D-Bus on Maemo to call it
2129 if player
== 'panucci':
2131 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2132 PANUCCI_PATH
= '/panucciInterface'
2133 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2134 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2135 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2137 def on_reply(*args
):
2140 def error_handler(filename
, err
):
2141 log('Exception in D-Bus call: %s', str(err
), \
2144 # Fallback: use the command line client
2145 for command
in util
.format_desktop_command('panucci', \
2147 log('Executing: %s', repr(command
), sender
=self
)
2148 subprocess
.Popen(command
)
2150 on_error
= lambda err
: error_handler(filename
, err
)
2152 # This method only exists in Panucci > 0.9 ('new Panucci')
2153 i
.playback_from(filename
, resume_position
, \
2154 reply_handler
=on_reply
, error_handler
=on_error
)
2156 continue # This file was handled by the D-Bus call
2157 except Exception, e
:
2158 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2159 elif player
== 'MediaBox' and gpodder
.ui
.fremantle
:
2161 MEDIABOX_NAME
= 'de.pycage.mediabox'
2162 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2163 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2164 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2165 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2167 def on_reply(*args
):
2171 log('Exception in D-Bus call: %s', str(err
), \
2174 i
.load(filename
, '%s/x-unknown' % file_type
, \
2175 reply_handler
=on_reply
, error_handler
=on_error
)
2177 continue # This file was handled by the D-Bus call
2178 except Exception, e
:
2179 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2181 groups
[player
].append(filename
)
2183 # Open episodes with system default player
2184 if 'default' in groups
:
2185 # Special-casing for a single episode when the object is a PDF
2186 # file - this is needed on Maemo 5, so we only use gui_open()
2187 # for single PDF files, but still use the built-in media player
2188 # with an M3U file for single audio/video files. (The Maemo 5
2189 # media player behaves differently when opening a single-file
2190 # M3U playlist compared to opening the single file directly.)
2191 if len(groups
['default']) == 1:
2192 fn
= groups
['default'][0]
2193 # The list of extensions is taken from gui_open in util.py
2194 # where all special-cases of Maemo apps are listed
2195 for extension
in ('.pdf', '.jpg', '.jpeg', '.png'):
2196 if fn
.lower().endswith(extension
):
2198 groups
['default'] = []
2201 if gpodder
.ui
.fremantle
and groups
['default']:
2202 # The Nokia Media Player app does not support receiving multiple
2203 # file names via D-Bus, so we simply place all file names into a
2204 # temporary M3U playlist and open that with the Media Player.
2205 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2209 return 'file://' + urllib
.quote(os
.path
.abspath(x
))
2212 util
.write_m3u_playlist(m3u_filename
, \
2213 map(to_url
, groups
['default']), \
2215 util
.gui_open(m3u_filename
)
2217 for filename
in groups
['default']:
2218 log('Opening with system default: %s', filename
, sender
=self
)
2219 util
.gui_open(filename
)
2220 del groups
['default']
2221 elif gpodder
.ui
.fremantle
and groups
:
2222 # When on Maemo and not opening with default, show a notification
2223 # (no startup notification for Panucci / MPlayer yet...)
2224 if len(episodes
) == 1:
2225 text
= _('Opening %s') % episodes
[0].title
2227 count
= len(episodes
)
2228 text
= N_('Opening %(count)d episode', 'Opening %(count)d episodes', count
) % {'count':count
}
2230 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2232 def destroy_banner_later(banner
):
2235 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2237 # For each type now, go and create play commands
2238 for group
in groups
:
2239 for command
in util
.format_desktop_command(group
, groups
[group
]):
2240 log('Executing: %s', repr(command
), sender
=self
)
2241 subprocess
.Popen(command
)
2243 # Persist episode status changes to the database
2246 # Flush updated episode status
2247 self
.mygpo_client
.flush()
2249 def playback_episodes(self
, episodes
):
2250 # We need to create a list, because we run through it more than once
2251 episodes
= list(Model
.sort_episodes_by_pubdate(e
for e
in episodes
if \
2252 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2255 self
.playback_episodes_for_real(episodes
)
2256 except Exception, e
:
2257 log('Error in playback!', sender
=self
, traceback
=True)
2258 if gpodder
.ui
.desktop
:
2259 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2260 _('Error opening player'), widget
=self
.toolPreferences
)
2262 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2264 channel_urls
= set()
2265 episode_urls
= set()
2266 for episode
in episodes
:
2267 channel_urls
.add(episode
.channel
.url
)
2268 episode_urls
.add(episode
.url
)
2269 self
.update_episode_list_icons(episode_urls
)
2270 self
.update_podcast_list_model(channel_urls
)
2272 def play_or_download(self
):
2273 if not gpodder
.ui
.fremantle
:
2274 if self
.wNotebook
.get_current_page() > 0:
2275 if gpodder
.ui
.desktop
:
2276 self
.toolCancel
.set_sensitive(True)
2279 if self
.currently_updating
:
2280 return (False, False, False, False, False, False)
2282 ( can_play
, can_download
, can_cancel
, can_delete
) = (False,)*4
2283 ( is_played
, is_locked
) = (False,)*2
2285 open_instead_of_play
= False
2287 selection
= self
.treeAvailable
.get_selection()
2288 if selection
.count_selected_rows() > 0:
2289 (model
, paths
) = selection
.get_selected_rows()
2293 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2294 except TypeError, te
:
2295 log('Invalid episode at path %s', str(path
), sender
=self
)
2298 if episode
.file_type() not in ('audio', 'video'):
2299 open_instead_of_play
= True
2301 if episode
.was_downloaded():
2302 can_play
= episode
.was_downloaded(and_exists
=True)
2303 is_played
= not episode
.is_new
2304 is_locked
= episode
.archive
2308 if self
.episode_is_downloading(episode
):
2313 can_download
= can_download
and not can_cancel
2314 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2315 can_delete
= not can_cancel
2317 if gpodder
.ui
.desktop
:
2318 if open_instead_of_play
:
2319 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2321 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2322 self
.toolPlay
.set_sensitive( can_play
)
2323 self
.toolDownload
.set_sensitive( can_download
)
2324 self
.toolCancel
.set_sensitive( can_cancel
)
2326 if not gpodder
.ui
.fremantle
:
2327 self
.item_cancel_download
.set_sensitive(can_cancel
)
2328 self
.itemDownloadSelected
.set_sensitive(can_download
)
2329 self
.itemOpenSelected
.set_sensitive(can_play
)
2330 self
.itemPlaySelected
.set_sensitive(can_play
)
2331 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2332 self
.item_toggle_played
.set_sensitive(can_play
)
2333 self
.item_toggle_lock
.set_sensitive(can_play
)
2334 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2335 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2337 return (can_play
, can_download
, can_cancel
, can_delete
, open_instead_of_play
)
2339 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2340 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2342 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2343 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2345 def episode_new_status_changed(self
, urls
):
2346 self
.update_podcast_list_model()
2347 self
.update_episode_list_icons(urls
)
2349 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2350 """Update the podcast list treeview model
2352 If urls is given, it should list the URLs of each
2353 podcast that has to be updated in the list.
2355 If selected is True, only update the model contents
2356 for the currently-selected podcast - nothing more.
2358 The caller can optionally specify "select_url",
2359 which is the URL of the podcast that is to be
2360 selected in the list after the update is complete.
2361 This only works if the podcast list has to be
2362 reloaded; i.e. something has been added or removed
2363 since the last update of the podcast list).
2365 selection
= self
.treeChannels
.get_selection()
2366 model
, iter = selection
.get_selected()
2368 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2369 # Update "all episodes" view in any case (if enabled)
2370 self
.podcast_list_model
.update_first_row()
2373 # very cheap! only update selected channel
2374 if iter is not None:
2375 # If we have selected the "all episodes" view, we have
2376 # to update all channels for selected episodes:
2377 if self
.config
.podcast_list_view_all
and \
2378 self
.podcast_list_model
.iter_is_first_row(iter):
2379 urls
= self
.get_podcast_urls_from_selected_episodes()
2380 self
.podcast_list_model
.update_by_urls(urls
)
2382 # Otherwise just update the selected row (a podcast)
2383 self
.podcast_list_model
.update_by_filter_iter(iter)
2384 elif not self
.channel_list_changed
:
2385 # we can keep the model, but have to update some
2387 # still cheaper than reloading the whole list
2388 self
.podcast_list_model
.update_all()
2390 # ok, we got a bunch of urls to update
2391 self
.podcast_list_model
.update_by_urls(urls
)
2393 if model
and iter and select_url
is None:
2394 # Get the URL of the currently-selected podcast
2395 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2397 # Update the podcast list model with new channels
2398 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2401 selected_iter
= model
.get_iter_first()
2402 # Find the previously-selected URL in the new
2403 # model if we have an URL (else select first)
2404 if select_url
is not None:
2405 pos
= model
.get_iter_first()
2406 while pos
is not None:
2407 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2408 if url
== select_url
:
2411 pos
= model
.iter_next(pos
)
2413 if not gpodder
.ui
.fremantle
:
2414 if selected_iter
is not None:
2415 selection
.select_iter(selected_iter
)
2416 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2418 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2419 self
.channel_list_changed
= False
2421 def episode_is_downloading(self
, episode
):
2422 """Returns True if the given episode is being downloaded at the moment"""
2426 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2428 def on_episode_list_filter_changed(self
, has_episodes
):
2429 if gpodder
.ui
.fremantle
:
2431 self
.episodes_window
.empty_label
.hide()
2432 self
.episodes_window
.pannablearea
.show()
2434 if self
.config
.episode_list_view_mode
!= \
2435 EpisodeListModel
.VIEW_ALL
:
2436 text
= _('No episodes in current view')
2438 text
= _('No episodes available')
2439 self
.episodes_window
.empty_label
.set_text(text
)
2440 self
.episodes_window
.pannablearea
.hide()
2441 self
.episodes_window
.empty_label
.show()
2443 def update_episode_list_model(self
):
2444 if self
.channels
and self
.active_channel
is not None:
2445 if gpodder
.ui
.fremantle
:
2446 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2448 self
.currently_updating
= True
2449 self
.episode_list_model
.clear()
2450 if gpodder
.ui
.fremantle
:
2451 self
.episodes_window
.pannablearea
.hide()
2452 self
.episodes_window
.empty_label
.set_text(_('Loading episodes'))
2453 self
.episodes_window
.empty_label
.show()
2456 additional_args
= (self
.episode_is_downloading
, \
2457 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
)
2458 self
.episode_list_model
.replace_from_channel(self
.active_channel
, *additional_args
)
2460 self
.treeAvailable
.get_selection().unselect_all()
2461 self
.treeAvailable
.scroll_to_point(0, 0)
2463 self
.currently_updating
= False
2464 self
.play_or_download()
2466 if gpodder
.ui
.fremantle
:
2467 hildon
.hildon_gtk_window_set_progress_indicator(\
2468 self
.episodes_window
.main_window
, False)
2470 util
.idle_add(update
)
2472 self
.episode_list_model
.clear()
2474 @dbus.service
.method(gpodder
.dbus_interface
)
2475 def offer_new_episodes(self
, channels
=None):
2476 if gpodder
.ui
.fremantle
:
2477 # Assume that when this function is called that the
2478 # notification is not shown anymore (Maemo bug 11345)
2479 self
._fremantle
_notification
_visible
= False
2481 new_episodes
= self
.get_new_episodes(channels
)
2483 self
.new_episodes_show(new_episodes
)
2487 def add_podcast_list(self
, urls
, auth_tokens
=None):
2488 """Subscribe to a list of podcast given their URLs
2490 If auth_tokens is given, it should be a dictionary
2491 mapping URLs to (username, password) tuples."""
2493 if auth_tokens
is None:
2496 # Sort and split the URL list into five buckets
2497 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2498 for input_url
in urls
:
2499 url
= util
.normalize_feed_url(input_url
)
2501 # Fail this one because the URL is not valid
2502 failed
.append(input_url
)
2503 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2504 # A podcast already exists in the list for this URL
2505 existing
.append(url
)
2507 # This URL has survived the first round - queue for add
2509 if url
!= input_url
and input_url
in auth_tokens
:
2510 auth_tokens
[url
] = auth_tokens
[input_url
]
2515 progress
= ProgressIndicator(_('Adding podcasts'), \
2516 _('Please wait while episode information is downloaded.'), \
2517 parent
=self
.get_dialog_parent())
2519 def on_after_update():
2520 progress
.on_finished()
2521 # Report already-existing subscriptions to the user
2523 title
= _('Existing subscriptions skipped')
2524 message
= _('You are already subscribed to these podcasts:') \
2525 + '\n\n' + '\n'.join(cgi
.escape(url
) for url
in existing
)
2526 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2528 # Report subscriptions that require authentication
2532 title
= _('Podcast requires authentication')
2533 message
= _('Please login to %s:') % (cgi
.escape(url
),)
2534 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2536 retry_podcasts
[url
] = auth_tokens
2538 # Stop asking the user for more login data
2541 error_messages
[url
] = _('Authentication failed')
2545 # If we have authentication data to retry, do so here
2547 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2549 # Report website redirections
2550 for url
in redirections
:
2551 title
= _('Website redirection detected')
2552 message
= _('The URL %(url)s redirects to %(target)s.') \
2553 + '\n\n' + _('Do you want to visit the website now?')
2554 message
= message
% {'url': url
, 'target': redirections
[url
]}
2555 if self
.show_confirmation(message
, title
):
2556 util
.open_website(url
)
2560 # Report failed subscriptions to the user
2562 title
= _('Could not add some podcasts')
2563 message
= _('Some podcasts could not be added to your list:') \
2564 + '\n\n' + '\n'.join(cgi
.escape('%s: %s' % (url
, \
2565 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2566 self
.show_message(message
, title
, important
=True)
2568 # Upload subscription changes to gpodder.net
2569 self
.mygpo_client
.on_subscribe(worked
)
2571 # If at least one podcast has been added, save and update all
2572 if self
.channel_list_changed
:
2573 # Fix URLs if mygpo has rewritten them
2574 self
.rewrite_urls_mygpo()
2576 # If only one podcast was added, select it after the update
2577 if len(worked
) == 1:
2582 # Update the list of subscribed podcasts
2583 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2585 # Offer to download new episodes
2587 for podcast
in self
.channels
:
2588 if podcast
.url
in worked
:
2589 episodes
.extend(podcast
.get_all_episodes())
2592 episodes
= list(Model
.sort_episodes_by_pubdate(episodes
, \
2594 self
.new_episodes_show(episodes
, \
2595 selected
=[e
.check_is_new() for e
in episodes
])
2599 # After the initial sorting and splitting, try all queued podcasts
2600 length
= len(queued
)
2601 for index
, url
in enumerate(queued
):
2602 progress
.on_progress(float(index
)/float(length
))
2603 progress
.on_message(url
)
2604 log('QUEUE RUNNER: %s', url
, sender
=self
)
2606 # The URL is valid and does not exist already - subscribe!
2607 channel
= Model
.load_podcast(self
.db
, url
=url
, create
=True, \
2608 authentication_tokens
=auth_tokens
.get(url
, None), \
2609 max_episodes
=self
.config
.max_episodes_per_feed
, \
2610 mimetype_prefs
=self
.config
.mimetype_prefs
)
2613 username
, password
= util
.username_password_from_url(url
)
2614 except ValueError, ve
:
2615 username
, password
= (None, None)
2617 if username
is not None and channel
.auth_username
is None and \
2618 password
is not None and channel
.auth_password
is None:
2619 channel
.auth_username
= username
2620 channel
.auth_password
= password
2623 self
._update
_cover
(channel
)
2624 except feedcore
.AuthenticationRequired
:
2625 if url
in auth_tokens
:
2626 # Fail for wrong authentication data
2627 error_messages
[url
] = _('Authentication failed')
2630 # Queue for login dialog later
2633 except feedcore
.WifiLogin
, error
:
2634 redirections
[url
] = error
.data
2636 error_messages
[url
] = _('Redirection detected')
2638 except Exception, e
:
2639 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2640 error_messages
[url
] = str(e
)
2644 assert channel
is not None
2645 worked
.append(channel
.url
)
2646 self
.channels
.append(channel
)
2647 self
.channel_list_changed
= True
2648 util
.idle_add(on_after_update
)
2649 threading
.Thread(target
=thread_proc
).start()
2651 def find_episode(self
, podcast_url
, episode_url
):
2652 """Find an episode given its podcast and episode URL
2654 The function will return a PodcastEpisode object if
2655 the episode is found, or None if it's not found.
2657 for podcast
in self
.channels
:
2658 if podcast_url
== podcast
.url
:
2659 for episode
in podcast
.get_all_episodes():
2660 if episode_url
== episode
.url
:
2665 def process_received_episode_actions(self
, updated_urls
):
2666 """Process/merge episode actions from gpodder.net
2668 This function will merge all changes received from
2669 the server to the local database and update the
2670 status of the affected episodes as necessary.
2672 indicator
= ProgressIndicator(_('Merging episode actions'), \
2673 _('Episode actions from gpodder.net are merged.'), \
2674 False, self
.get_dialog_parent())
2676 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2677 if action
.action
== 'play':
2678 episode
= self
.find_episode(action
.podcast_url
, \
2681 if episode
is not None:
2682 log('Play action for %s', episode
.url
, sender
=self
)
2683 episode
.mark(is_played
=True)
2685 if action
.timestamp
> episode
.current_position_updated
and \
2686 action
.position
is not None:
2687 log('Updating position for %s', episode
.url
, sender
=self
)
2688 episode
.current_position
= action
.position
2689 episode
.current_position_updated
= action
.timestamp
2692 log('Updating total time for %s', episode
.url
, sender
=self
)
2693 episode
.total_time
= action
.total
2696 elif action
.action
== 'delete':
2697 episode
= self
.find_episode(action
.podcast_url
, \
2700 if episode
is not None:
2701 if not episode
.was_downloaded(and_exists
=True):
2702 # Set the episode to a "deleted" state
2703 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2704 episode
.delete_from_disk()
2707 indicator
.on_message(N_('%(count)d action processed', '%(count)d actions processed', idx
) % {'count':idx
})
2708 gtk
.main_iteration(False)
2710 indicator
.on_finished()
2714 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2716 self
.updating_feed_cache
= False
2718 self
.channels
= Model
.get_podcasts(self
.db
)
2720 # Process received episode actions for all updated URLs
2721 self
.process_received_episode_actions(updated_urls
)
2723 self
.channel_list_changed
= True
2724 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2726 # Only search for new episodes in podcasts that have been
2727 # updated, not in other podcasts (for single-feed updates)
2728 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2730 if gpodder
.ui
.fremantle
:
2731 self
.fancy_progress_bar
.hide()
2732 self
.button_subscribe
.set_sensitive(True)
2733 self
.button_refresh
.set_sensitive(True)
2734 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2735 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2736 self
.update_episode_list_model()
2737 if self
.feed_cache_update_cancelled
:
2740 def application_in_foreground():
2742 return any(w
.get_property('is-topmost') for w
in hildon
.WindowStack
.get_default().get_windows())
2743 except Exception, e
:
2744 log('Could not determine is-topmost', traceback
=True)
2745 # When in doubt, assume not in foreground
2749 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2750 # New episodes found, but we should do nothing
2751 self
.show_message(_('New episodes are available.'))
2752 elif self
.config
.auto_download
== 'always' or \
2753 (self
.config
.auto_download
== 'wifi' and \
2754 self
.network_manager
.connection_is_wlan()):
2755 count
= len(episodes
)
2756 title
= N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count
) % {'count':count
}
2757 self
.show_message(title
)
2758 self
.download_episode_list(episodes
)
2759 elif self
.config
.auto_download
== 'queue':
2760 self
.show_message(_('New episodes have been added to the download list.'))
2761 self
.download_episode_list_paused(episodes
)
2762 elif application_in_foreground():
2763 if not self
._fremantle
_notification
_visible
:
2764 self
.new_episodes_show(episodes
)
2765 elif not self
._fremantle
_notification
_visible
:
2768 pynotify
.init('gPodder')
2769 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2770 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2771 n
.set_hint('dbus-callback-default', ' '.join([
2772 gpodder
.dbus_bus_name
,
2773 gpodder
.dbus_gui_object_path
,
2774 gpodder
.dbus_interface
,
2775 'offer_new_episodes',
2777 n
.set_category('gpodder-new-episodes')
2779 self
._fremantle
_notification
_visible
= True
2780 except Exception, e
:
2781 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2782 self
.new_episodes_show(episodes
)
2783 self
._fremantle
_notification
_visible
= False
2784 elif not self
.config
.auto_update_feeds
:
2785 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2788 if self
.feed_cache_update_cancelled
:
2789 # The user decided to abort the feed update
2790 self
.show_update_feeds_buttons()
2792 # Nothing new here - but inform the user
2793 self
.pbFeedUpdate
.set_fraction(1.0)
2794 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2795 self
.feed_cache_update_cancelled
= True
2796 self
.btnCancelFeedUpdate
.show()
2797 self
.btnCancelFeedUpdate
.set_sensitive(True)
2798 self
.itemUpdate
.set_sensitive(True)
2799 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2801 count
= len(episodes
)
2802 # New episodes are available
2803 self
.pbFeedUpdate
.set_fraction(1.0)
2804 # Are we minimized and should we auto download?
2805 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2806 self
.download_episode_list(episodes
)
2807 title
= N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count
) % {'count':count
}
2808 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2809 self
.show_update_feeds_buttons()
2810 elif self
.config
.auto_download
== 'queue':
2811 self
.download_episode_list_paused(episodes
)
2812 title
= N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count
) % {'count':count
}
2813 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2814 self
.show_update_feeds_buttons()
2816 self
.show_update_feeds_buttons()
2817 # New episodes are available and we are not minimized
2818 if not self
.config
.do_not_show_new_episodes_dialog
:
2819 self
.new_episodes_show(episodes
, notification
=True)
2821 message
= N_('%(count)d new episode available', '%(count)d new episodes available', count
) % {'count':count
}
2822 self
.pbFeedUpdate
.set_text(message
)
2824 def _update_cover(self
, channel
):
2825 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2826 self
.cover_downloader
.request_cover(channel
)
2828 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2829 total
= len(channels
)
2831 for updated
, channel
in enumerate(channels
):
2832 if not self
.feed_cache_update_cancelled
:
2834 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
, \
2835 mimetype_prefs
=self
.config
.mimetype_prefs
)
2836 self
._update
_cover
(channel
)
2837 except Exception, e
:
2838 d
= {'url': cgi
.escape(channel
.url
), 'message': cgi
.escape(str(e
))}
2840 message
= _('Error while updating %(url)s: %(message)s')
2842 message
= _('The feed at %(url)s could not be updated.')
2843 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2844 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2846 if self
.feed_cache_update_cancelled
:
2849 # By the time we get here the update may have already been cancelled
2850 if not self
.feed_cache_update_cancelled
:
2851 def update_progress():
2852 d
= {'podcast': channel
.title
, 'position': updated
+1, 'total': total
}
2853 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2854 self
.pbFeedUpdate
.set_text(progression
)
2855 self
.pbFeedUpdate
.set_fraction(float(updated
+1)/float(total
))
2856 util
.idle_add(update_progress
)
2858 updated_urls
= [c
.url
for c
in channels
]
2859 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2861 def show_update_feeds_buttons(self
):
2862 # Make sure that the buttons for updating feeds
2863 # appear - this should happen after a feed update
2864 self
.hboxUpdateFeeds
.hide()
2865 self
.btnUpdateFeeds
.show()
2866 self
.itemUpdate
.set_sensitive(True)
2867 self
.itemUpdateChannel
.set_sensitive(True)
2869 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2870 if not self
.feed_cache_update_cancelled
:
2871 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2872 self
.feed_cache_update_cancelled
= True
2873 if not gpodder
.ui
.fremantle
:
2874 self
.btnCancelFeedUpdate
.set_sensitive(False)
2875 elif not gpodder
.ui
.fremantle
:
2876 self
.show_update_feeds_buttons()
2878 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2879 if self
.updating_feed_cache
:
2880 if gpodder
.ui
.fremantle
:
2881 self
.feed_cache_update_cancelled
= True
2884 if not force_update
:
2885 self
.channels
= Model
.get_podcasts(self
.db
)
2886 self
.channel_list_changed
= True
2887 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2890 # Fix URLs if mygpo has rewritten them
2891 self
.rewrite_urls_mygpo()
2893 self
.updating_feed_cache
= True
2895 if channels
is None:
2896 # Only update podcasts for which updates are enabled
2897 channels
= [c
for c
in self
.channels
if not c
.pause_subscription
]
2899 if gpodder
.ui
.fremantle
:
2900 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2901 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2902 self
.fancy_progress_bar
.show()
2903 self
.button_subscribe
.set_sensitive(False)
2904 self
.button_refresh
.set_sensitive(False)
2905 self
.feed_cache_update_cancelled
= False
2907 self
.itemUpdate
.set_sensitive(False)
2908 self
.itemUpdateChannel
.set_sensitive(False)
2910 self
.feed_cache_update_cancelled
= False
2911 self
.btnCancelFeedUpdate
.show()
2912 self
.btnCancelFeedUpdate
.set_sensitive(True)
2913 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2914 self
.hboxUpdateFeeds
.show_all()
2915 self
.btnUpdateFeeds
.hide()
2917 if len(channels
) == 1:
2918 text
= _('Updating "%s"...') % channels
[0].title
2920 count
= len(channels
)
2921 text
= N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count
) % {'count':count
}
2922 self
.pbFeedUpdate
.set_text(text
)
2923 self
.pbFeedUpdate
.set_fraction(0)
2925 args
= (channels
, select_url_afterwards
)
2926 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2928 def on_gPodder_delete_event(self
, widget
, *args
):
2929 """Called when the GUI wants to close the window
2930 Displays a confirmation dialog (and closes/hides gPodder)
2933 downloading
= self
.download_status_model
.are_downloads_in_progress()
2936 if gpodder
.ui
.fremantle
:
2937 self
.close_gpodder()
2938 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2939 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2940 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
2942 title
= _('Quit gPodder')
2943 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2945 dialog
.set_title(title
)
2946 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2948 quit_button
.grab_focus()
2949 result
= dialog
.run()
2952 if result
== gtk
.RESPONSE_CLOSE
:
2953 self
.close_gpodder()
2955 self
.close_gpodder()
2959 def close_gpodder(self
):
2960 """ clean everything and exit properly
2964 # Notify all tasks to to carry out any clean-up actions
2965 self
.download_status_model
.tell_all_tasks_to_quit()
2967 while gtk
.events_pending():
2968 gtk
.main_iteration(False)
2970 self
.core
.shutdown()
2975 def get_expired_episodes(self
):
2976 for channel
in self
.channels
:
2977 for episode
in channel
.get_downloaded_episodes():
2978 # Never consider archived episodes as old
2982 # Never consider fresh episodes as old
2983 if episode
.age_in_days() < self
.config
.episode_old_age
:
2986 # Do not delete played episodes (except if configured)
2987 if not episode
.is_new
:
2988 if not self
.config
.auto_remove_played_episodes
:
2991 # Do not delete unplayed episodes (except if configured)
2993 if not self
.config
.auto_remove_unplayed_episodes
:
2998 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3003 episodes
= [e
for e
in episodes
if not e
.archive
]
3006 title
= _('Episodes are locked')
3007 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3008 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3011 count
= len(episodes
)
3012 title
= N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count
) % {'count':count
}
3013 message
= _('Deleting episodes removes downloaded files.')
3015 if gpodder
.ui
.fremantle
:
3016 message
= '\n'.join([title
, message
])
3018 if confirm
and not self
.show_confirmation(message
, title
):
3021 progress
= ProgressIndicator(_('Deleting episodes'), \
3022 _('Please wait while episodes are deleted'), \
3023 parent
=self
.get_dialog_parent())
3025 def finish_deletion(episode_urls
, channel_urls
):
3026 progress
.on_finished()
3028 # Episodes have been deleted - persist the database
3031 self
.update_episode_list_icons(episode_urls
)
3032 self
.update_podcast_list_model(channel_urls
)
3033 self
.play_or_download()
3036 episode_urls
= set()
3037 channel_urls
= set()
3039 episodes_status_update
= []
3040 for idx
, episode
in enumerate(episodes
):
3041 progress
.on_progress(float(idx
)/float(len(episodes
)))
3042 if episode
.archive
and skip_locked
:
3043 log('Not deleting episode (is locked): %s', episode
.title
)
3045 log('Deleting episode: %s', episode
.title
)
3046 progress
.on_message(episode
.title
)
3047 episode
.delete_from_disk()
3048 episode_urls
.add(episode
.url
)
3049 channel_urls
.add(episode
.channel
.url
)
3050 episodes_status_update
.append(episode
)
3052 # Tell the shownotes window that we have removed the episode
3053 if self
.episode_shownotes_window
is not None and \
3054 self
.episode_shownotes_window
.episode
is not None and \
3055 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3056 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3058 # Notify the web service about the status update + upload
3059 self
.mygpo_client
.on_delete(episodes_status_update
)
3060 self
.mygpo_client
.flush()
3062 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3064 threading
.Thread(target
=thread_proc
).start()
3068 def on_itemRemoveOldEpisodes_activate(self
, widget
):
3069 self
.show_delete_episodes_window()
3071 def show_delete_episodes_window(self
, channel
=None):
3072 """Offer deletion of episodes
3074 If channel is None, offer deletion of all episodes.
3075 Otherwise only offer deletion of episodes in the channel.
3078 ('markup_delete_episodes', None, None, _('Episode')),
3081 msg_older_than
= N_('Select older than %(count)d day', 'Select older than %(count)d days', self
.config
.episode_old_age
)
3082 selection_buttons
= {
3083 _('Select played'): lambda episode
: not episode
.is_new
,
3084 _('Select finished'): lambda episode
: episode
.is_finished(),
3085 msg_older_than
% {'count':self
.config
.episode_old_age
}: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3088 instructions
= _('Select the episodes you want to delete:')
3091 channels
= self
.channels
3093 channels
= [channel
]
3096 for channel
in channels
:
3097 for episode
in channel
.get_downloaded_episodes():
3098 # Disallow deletion of locked episodes that still exist
3099 if not episode
.archive
or not episode
.file_exists():
3100 episodes
.append(episode
)
3102 selected
= [not e
.is_new
or not e
.file_exists() for e
in episodes
]
3104 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3105 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3106 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3107 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3108 show_episode_shownotes
=self
.show_episode_shownotes
)
3110 def on_selected_episodes_status_changed(self
):
3111 # The order of the updates here is important! When "All episodes" is
3112 # selected, the update of the podcast list model depends on the episode
3113 # list selection to determine which podcasts are affected. Updating
3114 # the episode list could remove the selection if a filter is active.
3115 self
.update_podcast_list_model(selected
=True)
3116 self
.update_episode_list_icons(selected
=True)
3119 def mark_selected_episodes_new(self
):
3120 for episode
in self
.get_selected_episodes():
3122 self
.on_selected_episodes_status_changed()
3124 def mark_selected_episodes_old(self
):
3125 for episode
in self
.get_selected_episodes():
3127 self
.on_selected_episodes_status_changed()
3129 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3130 for episode
in self
.get_selected_episodes():
3132 episode
.mark(is_played
=episode
.is_new
)
3134 episode
.mark(is_played
=new_value
)
3135 self
.on_selected_episodes_status_changed()
3137 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3138 for episode
in self
.get_selected_episodes():
3140 episode
.mark(is_locked
=not episode
.archive
)
3142 episode
.mark(is_locked
=new_value
)
3143 self
.on_selected_episodes_status_changed()
3145 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3146 if self
.active_channel
is None:
3149 self
.active_channel
.auto_archive_episodes
= not self
.active_channel
.auto_archive_episodes
3150 self
.active_channel
.save()
3152 for episode
in self
.active_channel
.get_all_episodes():
3153 episode
.mark(is_locked
=self
.active_channel
.auto_archive_episodes
)
3155 self
.update_podcast_list_model(selected
=True)
3156 self
.update_episode_list_icons(all
=True)
3158 def on_itemUpdateChannel_activate(self
, widget
=None):
3159 if self
.active_channel
is None:
3160 title
= _('No podcast selected')
3161 message
= _('Please select a podcast in the podcasts list to update.')
3162 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3165 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3166 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3167 self
.update_feed_cache()
3169 self
.update_feed_cache(channels
=[self
.active_channel
])
3171 def on_itemUpdate_activate(self
, widget
=None):
3172 # Check if we have outstanding subscribe/unsubscribe actions
3173 if self
.on_add_remove_podcasts_mygpo():
3174 log('Update cancelled (received server changes)', sender
=self
)
3178 self
.update_feed_cache()
3180 gPodderWelcome(self
.gPodder
,
3181 center_on_widget
=self
.gPodder
,
3182 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3183 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3185 def download_episode_list_paused(self
, episodes
):
3186 self
.download_episode_list(episodes
, True)
3188 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3189 enable_update
= False
3191 for episode
in episodes
:
3192 log('Downloading episode: %s', episode
.title
, sender
= self
)
3193 if not episode
.was_downloaded(and_exists
=True):
3195 for task
in self
.download_tasks_seen
:
3196 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3197 self
.download_queue_manager
.add_task(task
, force_start
)
3198 enable_update
= True
3206 task
= download
.DownloadTask(episode
, self
.config
)
3207 except Exception, e
:
3208 d
= {'episode': episode
.title
, 'message': str(e
)}
3209 message
= _('Download error while downloading %(episode)s: %(message)s')
3210 self
.show_message(message
% d
, _('Download error'), important
=True)
3211 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3215 task
.status
= task
.PAUSED
3217 self
.mygpo_client
.on_download([task
.episode
])
3218 self
.download_queue_manager
.add_task(task
, force_start
)
3220 self
.download_status_model
.register_task(task
)
3221 enable_update
= True
3224 self
.enable_download_list_update()
3226 # Flush updated episode status
3227 self
.mygpo_client
.flush()
3229 def cancel_task_list(self
, tasks
):
3234 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3235 task
.status
= task
.CANCELLED
3236 elif task
.status
== task
.PAUSED
:
3237 task
.status
= task
.CANCELLED
3238 # Call run, so the partial file gets deleted
3241 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3242 self
.play_or_download()
3244 # Update the tab title and downloads list
3245 self
.update_downloads_list()
3247 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3249 ('markup_new_episodes', None, None, _('Episode')),
3251 show_notification
= notification
and gpodder
.ui
.fremantle
3253 instructions
= _('Select the episodes you want to download:')
3255 if self
.new_episodes_window
is not None:
3256 self
.new_episodes_window
.main_window
.destroy()
3257 self
.new_episodes_window
= None
3259 def download_episodes_callback(episodes
):
3260 self
.new_episodes_window
= None
3261 self
.download_episode_list(episodes
)
3263 if selected
is None:
3264 # Select all by default
3265 selected
= [True]*len(episodes
)
3267 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3268 title
=_('New episodes available'), \
3269 instructions
=instructions
, \
3270 episodes
=episodes
, \
3272 selected
=selected
, \
3273 stock_ok_button
= 'gpodder-download', \
3274 callback
=download_episodes_callback
, \
3275 remove_callback
=lambda e
: e
.mark_old(), \
3276 remove_action
=_('Mark as old'), \
3277 remove_finished
=self
.episode_new_status_changed
, \
3278 _config
=self
.config
, \
3279 show_notification
=show_notification
, \
3280 show_episode_shownotes
=self
.show_episode_shownotes
)
3282 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3283 if not self
.offer_new_episodes():
3284 self
.show_message(_('Please check for new episodes later.'), \
3285 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3287 def get_new_episodes(self
, channels
=None):
3288 if channels
is None:
3289 channels
= self
.channels
3291 for channel
in channels
:
3292 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3293 episodes
.append(episode
)
3297 def commit_changes_to_database(self
):
3298 """This will be called after the sync process is finished"""
3301 def on_itemShowAllEpisodes_activate(self
, widget
):
3302 self
.config
.podcast_list_view_all
= widget
.get_active()
3304 def on_itemShowToolbar_activate(self
, widget
):
3305 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3307 def on_itemShowDescription_activate(self
, widget
):
3308 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3310 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3311 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3312 if self
.config
.podcast_list_hide_boring
:
3313 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3315 self
.podcast_list_model
.set_view_mode(-1)
3317 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3319 if current
== self
.item_view_podcasts_all
:
3320 self
.podcast_list_model
.set_view_mode(-1)
3321 elif current
== self
.item_view_podcasts_downloaded
:
3322 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3323 elif current
== self
.item_view_podcasts_unplayed
:
3324 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3326 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3328 def on_item_view_episodes_changed(self
, radioaction
, current
):
3329 if current
== self
.item_view_episodes_all
:
3330 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_ALL
3331 elif current
== self
.item_view_episodes_undeleted
:
3332 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNDELETED
3333 elif current
== self
.item_view_episodes_downloaded
:
3334 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_DOWNLOADED
3335 elif current
== self
.item_view_episodes_unplayed
:
3336 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNPLAYED
3338 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3340 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3341 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3343 def properties_closed(self
):
3344 self
.preferences_dialog
= None
3346 def on_itemPreferences_activate(self
, widget
, *args
):
3347 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3348 _config
=self
.config
, \
3349 callback_finished
=self
.properties_closed
, \
3350 user_apps_reader
=self
.user_apps_reader
, \
3351 parent_window
=self
.main_window
, \
3352 mygpo_client
=self
.mygpo_client
, \
3353 on_send_full_subscriptions
=self
.on_send_full_subscriptions
, \
3354 on_itemExportChannels_activate
=self
.on_itemExportChannels_activate
)
3356 # Initial message to relayout window (in case it's opened in portrait mode
3357 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3359 def on_goto_mygpo(self
, widget
):
3360 self
.mygpo_client
.open_website()
3362 def on_download_subscriptions_from_mygpo(self
, action
=None):
3363 title
= _('Login to gpodder.net')
3364 message
= _('Please login to download your subscriptions.')
3365 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3366 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3370 self
.config
.mygpo_username
= username
3371 self
.config
.mygpo_password
= password
3373 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3374 custom_title
=_('Subscriptions on gpodder.net'), \
3375 add_urls_callback
=self
.add_podcast_list
, \
3376 hide_url_entry
=True)
3378 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3379 # we do not have to hardcode the URL here
3380 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3381 url
= util
.url_add_authentication(OPML_URL
, \
3382 self
.config
.mygpo_username
, \
3383 self
.config
.mygpo_password
)
3384 dir.download_opml_file(url
)
3386 def on_itemAddChannel_activate(self
, widget
=None):
3387 gPodderAddPodcast(self
.gPodder
, \
3388 add_urls_callback
=self
.add_podcast_list
)
3390 def on_itemEditChannel_activate(self
, widget
, *args
):
3391 if self
.active_channel
is None:
3392 title
= _('No podcast selected')
3393 message
= _('Please select a podcast in the podcasts list to edit.')
3394 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3397 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3398 gPodderChannel(self
.main_window
, \
3399 channel
=self
.active_channel
, \
3400 callback_closed
=callback_closed
, \
3401 cover_downloader
=self
.cover_downloader
)
3403 def on_itemMassUnsubscribe_activate(self
, item
=None):
3405 ('title', None, None, _('Podcast')),
3408 # We're abusing the Episode Selector for selecting Podcasts here,
3409 # but it works and looks good, so why not? -- thp
3410 gPodderEpisodeSelector(self
.main_window
, \
3411 title
=_('Remove podcasts'), \
3412 instructions
=_('Select the podcast you want to remove.'), \
3413 episodes
=self
.channels
, \
3415 size_attribute
=None, \
3416 stock_ok_button
=_('Remove'), \
3417 callback
=self
.remove_podcast_list
, \
3418 _config
=self
.config
)
3420 def remove_podcast_list(self
, channels
, confirm
=True):
3422 log('No podcasts selected for deletion', sender
=self
)
3425 if len(channels
) == 1:
3426 title
= _('Removing podcast')
3427 info
= _('Please wait while the podcast is removed')
3428 message
= _('Do you really want to remove this podcast and its episodes?')
3430 title
= _('Removing podcasts')
3431 info
= _('Please wait while the podcasts are removed')
3432 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3434 if confirm
and not self
.show_confirmation(message
, title
):
3437 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3439 def finish_deletion(select_url
):
3440 # Upload subscription list changes to the web service
3441 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3443 # Re-load the channels and select the desired new channel
3444 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3445 progress
.on_finished()
3450 for idx
, channel
in enumerate(channels
):
3451 # Update the UI for correct status messages
3452 progress
.on_progress(float(idx
)/float(len(channels
)))
3453 progress
.on_message(channel
.title
)
3455 # Delete downloaded episodes
3456 channel
.remove_downloaded()
3458 # cancel any active downloads from this channel
3459 for episode
in channel
.get_all_episodes():
3460 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3463 if len(channels
) == 1:
3464 # get the URL of the podcast we want to select next
3465 if channel
in self
.channels
:
3466 position
= self
.channels
.index(channel
)
3470 if position
== len(self
.channels
)-1:
3471 # this is the last podcast, so select the URL
3472 # of the item before this one (i.e. the "new last")
3473 select_url
= self
.channels
[position
-1].url
3475 # there is a podcast after the deleted one, so
3476 # we simply select the one that comes after it
3477 select_url
= self
.channels
[position
+1].url
3479 # Remove the channel and clean the database entries
3481 self
.channels
.remove(channel
)
3483 # Clean up downloads and download directories
3484 self
.clean_up_downloads()
3486 self
.channel_list_changed
= True
3488 # The remaining stuff is to be done in the GTK main thread
3489 util
.idle_add(finish_deletion
, select_url
)
3491 threading
.Thread(target
=thread_proc
).start()
3493 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3494 if self
.active_channel
is None:
3495 title
= _('No podcast selected')
3496 message
= _('Please select a podcast in the podcasts list to remove.')
3497 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3500 self
.remove_podcast_list([self
.active_channel
])
3502 def get_opml_filter(self
):
3503 filter = gtk
.FileFilter()
3504 filter.add_pattern('*.opml')
3505 filter.add_pattern('*.xml')
3506 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3509 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3510 if filename
is None:
3511 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3512 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), \
3513 parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3514 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3515 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3516 dlg
.set_filter(self
.get_opml_filter())
3517 response
= dlg
.run()
3519 if response
== gtk
.RESPONSE_OK
:
3520 filename
= dlg
.get_filename()
3523 if filename
is not None:
3524 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3525 custom_title
=_('Import podcasts from OPML file'), \
3526 add_urls_callback
=self
.add_podcast_list
, \
3527 hide_url_entry
=True)
3528 dir.download_opml_file(filename
)
3530 def on_itemExportChannels_activate(self
, widget
, *args
):
3531 if not self
.channels
:
3532 title
= _('Nothing to export')
3533 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3534 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3537 if gpodder
.ui
.desktop
:
3538 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3539 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3540 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3541 elif gpodder
.ui
.fremantle
:
3542 dlg
= gobject
.new(hildon
.FileChooserDialog
, \
3543 action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3544 dlg
.set_title(_('Export to OPML'))
3545 dlg
.set_filter(self
.get_opml_filter())
3546 response
= dlg
.run()
3547 if response
== gtk
.RESPONSE_OK
:
3548 filename
= dlg
.get_filename()
3550 exporter
= opml
.Exporter( filename
)
3551 if filename
is not None and exporter
.write(self
.channels
):
3552 count
= len(self
.channels
)
3553 title
= N_('%(count)d subscription exported', '%(count)d subscriptions exported', count
) % {'count':count
}
3554 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3556 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3560 def on_itemImportChannels_activate(self
, widget
, *args
):
3561 if gpodder
.ui
.fremantle
:
3562 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3563 self
.config
.toplist_opml
, \
3564 self
.config
.example_opml
, \
3565 self
.add_podcast_list
, \
3566 self
.on_itemAddChannel_activate
, \
3567 self
.on_download_subscriptions_from_mygpo
, \
3568 self
.show_text_edit_dialog
)
3570 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3571 add_urls_callback
=self
.add_podcast_list
)
3572 util
.idle_add(dir.download_opml_file
, self
.config
.example_opml
)
3574 def on_homepage_activate(self
, widget
, *args
):
3575 util
.open_website(gpodder
.__url
__)
3577 def on_wiki_activate(self
, widget
, *args
):
3578 util
.open_website('http://gpodder.org/wiki/User_Manual')
3580 def on_bug_tracker_activate(self
, widget
, *args
):
3581 if gpodder
.ui
.fremantle
:
3582 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3584 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder&component=Application&version=%s' % gpodder
.__version
__)
3586 def on_item_support_activate(self
, widget
):
3587 util
.open_website('http://gpodder.org/donate')
3589 def on_itemAbout_activate(self
, widget
, *args
):
3590 if gpodder
.ui
.fremantle
:
3591 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3592 HeAboutDialog
.present(self
.main_window
,
3595 gpodder
.__version
__,
3596 _('A podcast client with focus on usability'),
3597 gpodder
.__copyright
__,
3599 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3600 'http://gpodder.org/donate')
3603 dlg
= gtk
.Dialog(_('About gPodder'), self
.main_window
, \
3605 dlg
.set_resizable(False)
3607 bg
= gtk
.HBox(spacing
=10)
3608 bg
.pack_start(gtk
.image_new_from_file(gpodder
.icon_file
), expand
=False)
3611 label
.set_alignment(0, 1)
3612 label
.set_markup('<b><big>gPodder</big> %s</b>' % gpodder
.__version
__)
3613 vb
.pack_start(label
)
3615 label
.set_alignment(0, 0)
3616 label
.set_markup('<small>%s</small>' % \
3617 cgi
.escape(_('A podcast client with focus on usability')))
3618 vb
.pack_start(label
)
3620 label
.set_alignment(0, 0)
3621 label
.set_markup('<small><a href="%s">%s</a></small>' % \
3622 ((cgi
.escape(gpodder
.__url
__),)*2))
3623 vb
.pack_start(label
)
3626 out
= gtk
.VBox(spacing
=10)
3627 out
.set_border_width(12)
3628 out
.pack_start(bg
, expand
=False)
3629 out
.pack_start(gtk
.HSeparator())
3630 out
.pack_start(gtk
.Label(gpodder
.__copyright
__))
3632 button_box
= gtk
.HButtonBox()
3633 button
= gtk
.Button(_('Donate / Wishlist'))
3634 button
.connect('clicked', self
.on_item_support_activate
)
3635 button_box
.pack_start(button
)
3636 button
= gtk
.Button(_('Report a problem'))
3637 button
.connect('clicked', self
.on_bug_tracker_activate
)
3638 button_box
.pack_start(button
)
3639 out
.pack_start(button_box
, expand
=False)
3641 credits
= gtk
.TextView()
3642 credits
.set_left_margin(5)
3643 credits
.set_right_margin(5)
3644 credits
.set_pixels_above_lines(5)
3645 credits
.set_pixels_below_lines(5)
3646 credits
.set_editable(False)
3647 credits
.set_cursor_visible(False)
3648 sw
= gtk
.ScrolledWindow()
3649 sw
.set_shadow_type(gtk
.SHADOW_IN
)
3650 sw
.set_policy(gtk
.POLICY_NEVER
, gtk
.POLICY_AUTOMATIC
)
3652 credits
.set_size_request(-1, 160)
3653 out
.pack_start(sw
, expand
=True, fill
=True)
3655 dlg
.vbox
.pack_start(out
, expand
=False)
3656 dlg
.connect('response', lambda dlg
, response
: dlg
.destroy())
3660 if os
.path
.exists(gpodder
.credits_file
):
3661 credits_txt
= open(gpodder
.credits_file
).read().strip().split('\n')
3662 translator_credits
= _('translator-credits')
3663 if translator_credits
!= 'translator-credits':
3664 app_authors
= [_('Translation by:'), translator_credits
, '']
3668 app_authors
+= [_('Thanks to:')]
3669 app_authors
+= credits_txt
3671 buffer = gtk
.TextBuffer()
3672 buffer.set_text('\n'.join(app_authors
))
3673 credits
.set_buffer(buffer)
3677 credits
.grab_focus()
3678 dlg
.action_area
.hide()
3681 def on_wNotebook_switch_page(self
, notebook
, page
, page_num
):
3683 self
.play_or_download()
3684 # The message area in the downloads tab should be hidden
3685 # when the user switches away from the downloads tab
3686 if self
.message_area
is not None:
3687 self
.message_area
.hide()
3688 self
.message_area
= None
3689 elif gpodder
.ui
.desktop
:
3690 self
.toolDownload
.set_sensitive(False)
3691 self
.toolPlay
.set_sensitive(False)
3692 self
.toolCancel
.set_sensitive(False)
3694 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3695 # double-click action of the podcast list or enter
3696 self
.treeChannels
.set_cursor(path
)
3698 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3699 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3701 if model
is not None and iter is not None:
3702 old_active_channel
= self
.active_channel
3703 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3705 if self
.active_channel
== old_active_channel
:
3708 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3709 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3710 self
.itemEditChannel
.set_visible(False)
3711 self
.itemRemoveChannel
.set_visible(False)
3713 self
.itemEditChannel
.set_visible(True)
3714 self
.itemRemoveChannel
.set_visible(True)
3716 self
.active_channel
= None
3717 self
.itemEditChannel
.set_visible(False)
3718 self
.itemRemoveChannel
.set_visible(False)
3720 self
.update_episode_list_model()
3722 def on_btnEditChannel_clicked(self
, widget
, *args
):
3723 self
.on_itemEditChannel_activate( widget
, args
)
3725 def get_podcast_urls_from_selected_episodes(self
):
3726 """Get a set of podcast URLs based on the selected episodes"""
3727 return set(episode
.channel
.url
for episode
in \
3728 self
.get_selected_episodes())
3730 def get_selected_episodes(self
):
3731 """Get a list of selected episodes from treeAvailable"""
3732 selection
= self
.treeAvailable
.get_selection()
3733 model
, paths
= selection
.get_selected_rows()
3735 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3738 def on_playback_selected_episodes(self
, widget
):
3739 self
.playback_episodes(self
.get_selected_episodes())
3741 def on_shownotes_selected_episodes(self
, widget
):
3742 episodes
= self
.get_selected_episodes()
3744 episode
= episodes
.pop(0)
3745 self
.show_episode_shownotes(episode
)
3747 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3749 def on_download_selected_episodes(self
, widget
):
3750 episodes
= self
.get_selected_episodes()
3751 self
.download_episode_list(episodes
)
3752 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3753 self
.play_or_download()
3755 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3756 """Double-click/enter action handler for treeAvailable"""
3757 self
.on_shownotes_selected_episodes(widget
)
3759 def show_episode_shownotes(self
, episode
):
3760 if self
.episode_shownotes_window
is None:
3761 log('First-time use of episode window --- creating', sender
=self
)
3762 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3763 _download_episode_list
=self
.download_episode_list
, \
3764 _playback_episodes
=self
.playback_episodes
, \
3765 _delete_episode_list
=self
.delete_episode_list
, \
3766 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3767 _cancel_task_list
=self
.cancel_task_list
, \
3768 _episode_is_downloading
=self
.episode_is_downloading
, \
3769 _streaming_possible
=self
.streaming_possible())
3770 self
.episode_shownotes_window
.show(episode
)
3771 if self
.episode_is_downloading(episode
):
3772 self
.update_downloads_list()
3774 def restart_auto_update_timer(self
):
3775 if self
._auto
_update
_timer
_source
_id
is not None:
3776 log('Removing existing auto update timer.', sender
=self
)
3777 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
3778 self
._auto
_update
_timer
_source
_id
= None
3780 if self
.config
.auto_update_feeds
and \
3781 self
.config
.auto_update_frequency
:
3782 interval
= 60*1000*self
.config
.auto_update_frequency
3783 log('Setting up auto update timer with interval %d.', \
3784 self
.config
.auto_update_frequency
, sender
=self
)
3785 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
3786 interval
, self
._on
_auto
_update
_timer
)
3788 def _on_auto_update_timer(self
):
3789 log('Auto update timer fired.', sender
=self
)
3790 self
.update_feed_cache(force_update
=True)
3792 # Ask web service for sub changes (if enabled)
3793 self
.mygpo_client
.flush()
3797 def on_treeDownloads_row_activated(self
, widget
, *args
):
3798 # Use the standard way of working on the treeview
3799 selection
= self
.treeDownloads
.get_selection()
3800 (model
, paths
) = selection
.get_selected_rows()
3801 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
3803 for tree_row_reference
, task
in selected_tasks
:
3804 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
3805 task
.status
= task
.PAUSED
3806 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
3807 self
.download_queue_manager
.add_task(task
)
3808 self
.enable_download_list_update()
3809 elif task
.status
== task
.DONE
:
3810 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
3812 self
.play_or_download()
3814 # Update the tab title and downloads list
3815 self
.update_downloads_list()
3817 def on_item_cancel_download_activate(self
, widget
):
3818 if self
.wNotebook
.get_current_page() == 0:
3819 selection
= self
.treeAvailable
.get_selection()
3820 (model
, paths
) = selection
.get_selected_rows()
3821 urls
= [model
.get_value(model
.get_iter(path
), \
3822 self
.episode_list_model
.C_URL
) for path
in paths
]
3823 selected_tasks
= [task
for task
in self
.download_tasks_seen \
3824 if task
.url
in urls
]
3826 selection
= self
.treeDownloads
.get_selection()
3827 (model
, paths
) = selection
.get_selected_rows()
3828 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
3829 self
.download_status_model
.C_TASK
) for path
in paths
]
3830 self
.cancel_task_list(selected_tasks
)
3832 def on_btnCancelAll_clicked(self
, widget
, *args
):
3833 self
.cancel_task_list(self
.download_tasks_seen
)
3835 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
3836 episodes
= self
.get_selected_episodes()
3837 if len(episodes
) == 1:
3838 self
.delete_episode_list(episodes
, skip_locked
=False)
3840 self
.delete_episode_list(episodes
)
3842 def on_key_press(self
, widget
, event
):
3843 # Allow tab switching with Ctrl + PgUp/PgDown
3844 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
3845 if event
.keyval
== gtk
.keysyms
.Page_Up
:
3846 self
.wNotebook
.prev_page()
3848 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
3849 self
.wNotebook
.next_page()
3854 def uniconify_main_window(self
):
3855 if self
.is_iconified():
3856 # We need to hide and then show the window in WMs like Metacity
3857 # or KWin4 to move the window to the active workspace
3858 # (see http://gpodder.org/bug/1125)
3861 self
.gPodder
.present()
3863 def iconify_main_window(self
):
3864 if not self
.is_iconified():
3865 self
.gPodder
.iconify()
3867 @dbus.service
.method(gpodder
.dbus_interface
)
3868 def show_gui_window(self
):
3869 parent
= self
.get_dialog_parent()
3872 @dbus.service
.method(gpodder
.dbus_interface
)
3873 def subscribe_to_url(self
, url
):
3874 gPodderAddPodcast(self
.gPodder
,
3875 add_urls_callback
=self
.add_podcast_list
,
3878 @dbus.service
.method(gpodder
.dbus_interface
)
3879 def mark_episode_played(self
, filename
):
3880 if filename
is None:
3883 for channel
in self
.channels
:
3884 for episode
in channel
.get_all_episodes():
3885 fn
= episode
.local_filename(create
=False, check_only
=True)
3887 episode
.mark(is_played
=True)
3889 self
.update_episode_list_icons([episode
.url
])
3890 self
.update_podcast_list_model([episode
.channel
.url
])
3896 def main(options
=None):
3897 gobject
.threads_init()
3898 gobject
.set_application_name('gPodder')
3900 if gpodder
.ui
.fremantle
:
3901 # Add custom icons for the new Maemo 5 look :)
3902 for id in ('audio', 'video', 'download', 'audio-locked', 'video-locked'):
3903 filename
= os
.path
.join(gpodder
.images_folder
, '%s.png' % id)
3904 pixbuf
= gtk
.gdk
.pixbuf_new_from_file(filename
)
3905 gtk
.icon_theme_add_builtin_icon('gpodder-%s' % id, 40, pixbuf
)
3907 gtk
.window_set_default_icon_name('gpodder')
3908 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
3911 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
3912 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
3914 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
3915 except dbus
.exceptions
.DBusException
, dbe
:
3916 log('Warning: Cannot get "on the bus".', traceback
=True)
3917 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
3918 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
3919 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
3920 dlg
.set_title('gPodder')
3925 gp
= gPodder(bus_name
, core
.Core(UIConfig
))
3928 if options
.subscribe
:
3929 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
3932 # handle "subscribe to podcast" events from firefox
3933 if platform
.system() == 'Darwin':
3934 from gpodder
import gpodderosx
3935 gpodderosx
.register_handlers(gp
)
3936 # end mac OS X stuff