1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
36 from xml
.sax
import saxutils
46 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
49 def __init__(self
, *args
, **kwargs
):
51 def add_signal_receiver(self
, *args
, **kwargs
):
55 def __init__(self
, *args
, **kwargs
):
59 def method(*args
, **kwargs
):
62 def __init__(self
, *args
, **kwargs
):
65 def __init__(self
, *args
, **kwargs
):
69 from gpodder
import feedcore
70 from gpodder
import util
71 from gpodder
import opml
72 from gpodder
import download
73 from gpodder
import my
74 from gpodder
import youtube
75 from gpodder
import player
76 from gpodder
.liblogger
import log
81 from gpodder
.model
import PodcastChannel
82 from gpodder
.model
import PodcastEpisode
83 from gpodder
.dbsqlite
import Database
85 from gpodder
.gtkui
.model
import PodcastListModel
86 from gpodder
.gtkui
.model
import EpisodeListModel
87 from gpodder
.gtkui
.config
import UIConfig
88 from gpodder
.gtkui
.services
import CoverDownloader
89 from gpodder
.gtkui
.widgets
import SimpleMessageArea
90 from gpodder
.gtkui
.desktopfile
import UserAppsReader
92 from gpodder
.gtkui
.draw
import draw_text_box_centered
94 from gpodder
.gtkui
.interface
.common
import BuilderWidget
95 from gpodder
.gtkui
.interface
.common
import TreeViewHelper
96 from gpodder
.gtkui
.interface
.addpodcast
import gPodderAddPodcast
98 if gpodder
.ui
.desktop
:
99 from gpodder
.gtkui
.download
import DownloadStatusModel
101 from gpodder
.gtkui
.desktop
.sync
import gPodderSyncUI
103 from gpodder
.gtkui
.desktop
.channel
import gPodderChannel
104 from gpodder
.gtkui
.desktop
.preferences
import gPodderPreferences
105 from gpodder
.gtkui
.desktop
.shownotes
import gPodderShownotes
106 from gpodder
.gtkui
.desktop
.episodeselector
import gPodderEpisodeSelector
107 from gpodder
.gtkui
.desktop
.podcastdirectory
import gPodderPodcastDirectory
108 from gpodder
.gtkui
.desktop
.dependencymanager
import gPodderDependencyManager
109 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
111 from gpodder
.gtkui
.desktop
.trayicon
import GPodderStatusIcon
113 except Exception, exc
:
114 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
115 log('Warning: This probably means your PyGTK installation is too old!')
116 have_trayicon
= False
117 elif gpodder
.ui
.diablo
:
118 from gpodder
.gtkui
.download
import DownloadStatusModel
120 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
121 from gpodder
.gtkui
.maemo
.preferences
import gPodderPreferences
122 from gpodder
.gtkui
.maemo
.shownotes
import gPodderShownotes
123 from gpodder
.gtkui
.maemo
.episodeselector
import gPodderEpisodeSelector
124 from gpodder
.gtkui
.maemo
.podcastdirectory
import gPodderPodcastDirectory
125 from gpodder
.gtkui
.maemo
.mygpodder
import MygPodderSettings
126 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
127 have_trayicon
= False
128 elif gpodder
.ui
.fremantle
:
129 from gpodder
.gtkui
.frmntl
.model
import DownloadStatusModel
130 from gpodder
.gtkui
.frmntl
.model
import EpisodeListModel
131 from gpodder
.gtkui
.frmntl
.model
import PodcastListModel
133 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
134 from gpodder
.gtkui
.frmntl
.preferences
import gPodderPreferences
135 from gpodder
.gtkui
.frmntl
.shownotes
import gPodderShownotes
136 from gpodder
.gtkui
.frmntl
.episodeselector
import gPodderEpisodeSelector
137 from gpodder
.gtkui
.frmntl
.podcastdirectory
import gPodderPodcastDirectory
138 from gpodder
.gtkui
.frmntl
.episodes
import gPodderEpisodes
139 from gpodder
.gtkui
.frmntl
.downloads
import gPodderDownloads
140 from gpodder
.gtkui
.frmntl
.progress
import ProgressIndicator
141 from gpodder
.gtkui
.frmntl
.widgets
import FancyProgressBar
142 have_trayicon
= False
144 from gpodder
.gtkui
.frmntl
.portrait
import FremantleRotation
145 from gpodder
.gtkui
.frmntl
.mafw
import MafwPlaybackMonitor
146 from gpodder
.gtkui
.frmntl
.hints
import HINT_STRINGS
148 from gpodder
.gtkui
.interface
.common
import Orientation
150 from gpodder
.gtkui
.interface
.welcome
import gPodderWelcome
155 from gpodder
.dbusproxy
import DBusPodcastsProxy
156 from gpodder
import hooks
158 class gPodder(BuilderWidget
, dbus
.service
.Object
):
159 finger_friendly_widgets
= ['btnCleanUpDownloads', 'button_search_episodes_clear', 'label2', 'labelDownloads', 'btnUpdateFeeds']
161 ICON_GENERAL_ADD
= 'general_add'
162 ICON_GENERAL_REFRESH
= 'general_refresh'
164 # Delay until live search is started after typing stop
165 LIVE_SEARCH_DELAY
= 200
167 def __init__(self
, bus_name
, config
):
168 dbus
.service
.Object
.__init
__(self
, object_path
=gpodder
.dbus_gui_object_path
, bus_name
=bus_name
)
169 self
.podcasts_proxy
= DBusPodcastsProxy(lambda: self
.channels
, \
170 self
.on_itemUpdate_activate
, \
171 self
.playback_episodes
, \
172 self
.download_episode_list
, \
173 self
.episode_object_by_uri
, \
175 self
.db
= Database(gpodder
.database_file
)
177 BuilderWidget
.__init
__(self
, None)
180 if gpodder
.ui
.diablo
:
182 self
.app
= hildon
.Program()
183 self
.app
.add_window(self
.main_window
)
184 self
.main_window
.add_toolbar(self
.toolbar
)
186 for child
in self
.main_menu
.get_children():
188 self
.main_window
.set_menu(self
.set_finger_friendly(menu
))
189 self
._last
_orientation
= Orientation
.LANDSCAPE
190 elif gpodder
.ui
.fremantle
:
192 self
.app
= hildon
.Program()
193 self
.app
.add_window(self
.main_window
)
195 appmenu
= hildon
.AppMenu()
197 for filter in (self
.item_view_podcasts_all
, \
198 self
.item_view_podcasts_downloaded
, \
199 self
.item_view_podcasts_unplayed
):
200 button
= gtk
.ToggleButton()
201 filter.connect_proxy(button
)
202 appmenu
.add_filter(button
)
204 for action
in (self
.itemPreferences
, \
205 self
.item_downloads
, \
206 self
.itemRemoveOldEpisodes
, \
207 self
.item_unsubscribe
, \
209 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
210 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
211 action
.connect_proxy(button
)
212 if action
== self
.item_downloads
:
213 button
.set_title(_('Downloads'))
214 button
.set_value(_('Idle'))
215 self
.button_downloads
= button
216 appmenu
.append(button
)
218 def show_hint(button
):
219 self
.show_message(random
.choice(HINT_STRINGS
), important
=True)
221 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
222 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
223 button
.set_title(_('Hint of the day'))
224 button
.connect('clicked', show_hint
)
225 appmenu
.append(button
)
228 self
.main_window
.set_app_menu(appmenu
)
230 # Initialize portrait mode / rotation manager
231 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
233 gpodder
.__version
__, \
234 self
.config
.rotation_mode
)
236 if self
.config
.rotation_mode
== FremantleRotation
.ALWAYS
:
237 util
.idle_add(self
.on_window_orientation_changed
, \
238 Orientation
.PORTRAIT
)
239 self
._last
_orientation
= Orientation
.PORTRAIT
241 self
._last
_orientation
= Orientation
.LANDSCAPE
243 # Flag set when a notification is being shown (Maemo bug 11235)
244 self
._fremantle
_notification
_visible
= False
246 self
._last
_orientation
= Orientation
.LANDSCAPE
247 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
249 self
.bluetooth_available
= util
.bluetooth_available()
251 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
252 if not gpodder
.ui
.fremantle
:
253 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
254 self
.main_window
.show()
256 self
.player_receiver
= player
.MediaPlayerDBusReceiver(self
.on_played
)
258 if gpodder
.ui
.fremantle
:
259 # Create a D-Bus monitoring object that takes care of
260 # tracking MAFW (Nokia Media Player) playback events
261 # and sends episode playback status events via D-Bus
262 self
.mafw_monitor
= MafwPlaybackMonitor(gpodder
.dbus_session_bus
)
264 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
266 self
.preferences_dialog
= None
267 self
.config
.add_observer(self
.on_config_changed
)
269 self
.tray_icon
= None
270 self
.episode_shownotes_window
= None
271 self
.new_episodes_window
= None
273 if gpodder
.ui
.desktop
:
274 # Mac OS X-specific UI tweaks: Native main menu integration
275 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
276 if getattr(gtk
.gdk
, 'WINDOWING', 'x11') == 'quartz':
278 import igemacintegration
as igemi
280 # Move the menu bar from the window to the Mac menu bar
282 igemi
.ige_mac_menu_set_menu_bar(self
.mainMenu
)
284 # Reparent some items to the "Application" menu
285 for widget
in ('/mainMenu/menuHelp/itemAbout', \
286 '/mainMenu/menuPodcasts/itemPreferences'):
287 item
= self
.uimanager1
.get_widget(widget
)
288 group
= igemi
.ige_mac_menu_add_app_menu_group()
289 igemi
.ige_mac_menu_add_app_menu_item(group
, item
, None)
291 quit_widget
= '/mainMenu/menuPodcasts/itemQuit'
292 quit_item
= self
.uimanager1
.get_widget(quit_widget
)
293 igemi
.ige_mac_menu_set_quit_menu_item(quit_item
)
295 print >>sys
.stderr
, """
296 Warning: ige-mac-integration not found - no native menus.
299 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
300 self
.main_window
, self
.show_confirmation
, \
301 self
.update_episode_list_icons
, \
302 self
.update_podcast_list_model
, self
.toolPreferences
, \
303 gPodderEpisodeSelector
, \
304 self
.commit_changes_to_database
)
308 self
.download_status_model
= DownloadStatusModel()
309 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
311 if gpodder
.ui
.desktop
:
312 self
.show_hide_tray_icon()
313 self
.itemShowAllEpisodes
.set_active(self
.config
.podcast_list_view_all
)
314 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
315 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
317 if not gpodder
.ui
.fremantle
:
318 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
319 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
320 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
321 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
323 # When the amount of maximum downloads changes, notify the queue manager
324 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_threads()
325 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
327 self
.default_title
= 'gPodder'
328 if gpodder
.__version
__.rfind('git') != -1:
329 self
.set_title('gPodder %s' % gpodder
.__version
__)
331 title
= self
.gPodder
.get_title()
332 if title
is not None:
333 self
.set_title(title
)
335 self
.set_title(_('gPodder'))
337 self
.cover_downloader
= CoverDownloader()
339 # Generate list models for podcasts and their episodes
340 self
.podcast_list_model
= PodcastListModel(self
.cover_downloader
)
342 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
343 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
345 if gpodder
.ui
.fremantle
:
346 # Work around Maemo bug #4718
347 self
.button_refresh
.set_name('HildonButton-finger')
348 self
.button_subscribe
.set_name('HildonButton-finger')
350 self
.button_refresh
.set_sensitive(False)
351 self
.button_subscribe
.set_sensitive(False)
353 self
.button_subscribe
.set_image(gtk
.image_new_from_icon_name(\
354 self
.ICON_GENERAL_ADD
, gtk
.ICON_SIZE_BUTTON
))
355 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
356 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
358 # Make the button scroll together with the TreeView contents
359 action_area_box
= self
.treeChannels
.get_action_area_box()
360 for child
in self
.buttonbox
:
361 child
.reparent(action_area_box
)
362 self
.vbox
.remove(self
.buttonbox
)
363 action_area_box
.set_spacing(2)
364 action_area_box
.set_border_width(3)
365 self
.treeChannels
.set_action_area_visible(True)
367 # Set up a very nice progress bar setup
368 self
.fancy_progress_bar
= FancyProgressBar(self
.main_window
, \
369 self
.on_btnCancelFeedUpdate_clicked
)
370 self
.pbFeedUpdate
= self
.fancy_progress_bar
.progress_bar
371 self
.pbFeedUpdate
.set_ellipsize(pango
.ELLIPSIZE_MIDDLE
)
372 self
.vbox
.pack_start(self
.fancy_progress_bar
.event_box
, False)
374 from gpodder
.gtkui
.frmntl
import style
375 sub_font
= style
.get_font_desc('SmallSystemFont')
376 sub_color
= style
.get_color('SecondaryTextColor')
377 sub
= (sub_font
.to_string(), sub_color
.to_string())
378 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
379 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
381 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
382 while gtk
.events_pending():
383 gtk
.main_iteration(False)
386 # Try to get the real package version from dpkg
387 p
= subprocess
.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout
=subprocess
.PIPE
)
388 version
, _stderr
= p
.communicate()
392 version
= gpodder
.__version
__
393 self
.label_footer
.set_markup(sub
% ('v %s' % version
))
394 self
.label_footer
.hide()
396 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
397 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
398 show_episode_shownotes
=self
.show_episode_shownotes
, \
399 update_podcast_list_model
=self
.update_podcast_list_model
, \
400 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
401 item_view_episodes_all
=self
.item_view_episodes_all
, \
402 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
403 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
404 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
405 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
406 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
407 hide_episode_search
=self
.hide_episode_search
, \
408 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
409 playback_episodes
=self
.playback_episodes
, \
410 delete_episode_list
=self
.delete_episode_list
, \
411 episode_list_status_changed
=self
.episode_list_status_changed
, \
412 download_episode_list
=self
.download_episode_list
, \
413 episode_is_downloading
=self
.episode_is_downloading
, \
414 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
415 add_download_task_monitor
=self
.add_download_task_monitor
, \
416 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
417 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
, \
418 on_itemUpdate_activate
=self
.on_itemUpdate_activate
, \
419 show_delete_episodes_window
=self
.show_delete_episodes_window
, \
420 cover_downloader
=self
.cover_downloader
)
422 # Expose objects for episode list type-ahead find
423 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
424 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
425 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
427 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
428 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
429 cleanup_downloads
=self
.cleanup_downloads
, \
430 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
431 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
434 self
.treeAvailable
= self
.episodes_window
.treeview
435 self
.treeDownloads
= self
.downloads_window
.treeview
437 # Source IDs for timeouts for search-as-you-type
438 self
._podcast
_list
_search
_timeout
= None
439 self
._episode
_list
_search
_timeout
= None
441 # Init the treeviews that we use
442 self
.init_podcast_list_treeview()
443 self
.init_episode_list_treeview()
444 self
.init_download_list_treeview()
446 if self
.config
.podcast_list_hide_boring
:
447 self
.item_view_hide_boring_podcasts
.set_active(True)
449 self
.currently_updating
= False
451 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
452 self
.context_menu_mouse_button
= 1
454 self
.context_menu_mouse_button
= 3
456 if self
.config
.start_iconified
:
457 self
.iconify_main_window()
459 self
.download_tasks_seen
= set()
460 self
.download_list_update_enabled
= False
461 self
.download_task_monitors
= set()
463 # Subscribed channels
464 self
.active_channel
= None
465 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
466 self
.channel_list_changed
= True
467 self
.update_podcasts_tab()
469 # load list of user applications for audio playback
470 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
471 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
473 # Set the "Device" menu item for the first time
474 if gpodder
.ui
.desktop
:
475 self
.update_item_device()
477 # Set up the first instance of MygPoClient
478 self
.mygpo_client
= my
.MygPoClient(self
.config
)
480 # Now, update the feed cache, when everything's in place
481 if not gpodder
.ui
.fremantle
:
482 self
.btnUpdateFeeds
.show()
483 self
.updating_feed_cache
= False
484 self
.feed_cache_update_cancelled
= False
485 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
487 self
.message_area
= None
489 def find_partial_downloads():
490 # Look for partial file downloads
491 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
492 count
= len(partial_files
)
493 resumable_episodes
= []
495 if not gpodder
.ui
.fremantle
:
496 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
497 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
498 _('Some episodes have not finished downloading in a previous session.'), \
499 False, self
.get_dialog_parent())
500 indicator
.on_message(N_('%(count)d partial file', '%(count)d partial files', count
) % {'count':count
})
502 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
505 for c
in self
.channels
:
506 for e
in c
.get_all_episodes():
507 filename
= e
.local_filename(create
=False, check_only
=True)
508 if filename
in candidates
:
509 log('Found episode: %s', e
.title
, sender
=self
)
511 indicator
.on_message(e
.title
)
512 indicator
.on_progress(float(found
)/count
)
513 candidates
.remove(filename
)
514 partial_files
.remove(filename
+'.partial')
515 resumable_episodes
.append(e
)
523 for f
in partial_files
:
524 log('Partial file without episode: %s', f
, sender
=self
)
527 util
.idle_add(indicator
.on_finished
)
529 if len(resumable_episodes
):
530 def offer_resuming():
531 self
.download_episode_list_paused(resumable_episodes
)
532 if not gpodder
.ui
.fremantle
:
533 resume_all
= gtk
.Button(_('Resume all'))
534 #resume_all.set_border_width(0)
535 def on_resume_all(button
):
536 selection
= self
.treeDownloads
.get_selection()
537 selection
.select_all()
538 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
539 selection
.unselect_all()
540 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
541 self
.message_area
.hide()
542 resume_all
.connect('clicked', on_resume_all
)
544 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
545 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
546 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
547 self
.message_area
.show_all()
548 self
.clean_up_downloads(delete_partial
=False)
549 util
.idle_add(offer_resuming
)
550 elif not gpodder
.ui
.fremantle
:
551 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
553 util
.idle_add(self
.clean_up_downloads
, True)
554 threading
.Thread(target
=find_partial_downloads
).start()
556 # Start the auto-update procedure
557 self
._auto
_update
_timer
_source
_id
= None
558 if self
.config
.auto_update_feeds
:
559 self
.restart_auto_update_timer()
561 # Delete old episodes if the user wishes to
562 if self
.config
.auto_remove_played_episodes
and \
563 self
.config
.episode_old_age
> 0:
564 old_episodes
= list(self
.get_expired_episodes())
565 if len(old_episodes
) > 0:
566 self
.delete_episode_list(old_episodes
, confirm
=False)
567 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
569 if gpodder
.ui
.fremantle
:
570 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
571 self
.button_refresh
.set_sensitive(True)
572 self
.button_subscribe
.set_sensitive(True)
573 self
.main_window
.set_title(_('gPodder'))
574 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
576 # Do the initial sync with the web service
577 util
.idle_add(self
.mygpo_client
.flush
, True)
579 # First-time users should be asked if they want to see the OPML
580 if not self
.channels
and not gpodder
.ui
.fremantle
:
581 util
.idle_add(self
.on_itemUpdate_activate
)
583 def episode_object_by_uri(self
, uri
):
584 """Get an episode object given a local or remote URI
586 This can be used to quickly access an episode object
587 when all we have is its download filename or episode
588 URL (e.g. from external D-Bus calls / signals, etc..)
590 if uri
.startswith('/'):
591 uri
= 'file://' + uri
593 prefix
= 'file://' + self
.config
.download_dir
595 if uri
.startswith(prefix
):
596 # File is on the local filesystem in the download folder
597 filename
= uri
[len(prefix
):]
598 file_parts
= [x
for x
in filename
.split(os
.sep
) if x
]
600 if len(file_parts
) == 2:
601 dir_name
, filename
= file_parts
602 channels
= [c
for c
in self
.channels
if c
.foldername
== dir_name
]
603 if len(channels
) == 1:
604 channel
= channels
[0]
605 return channel
.get_episode_by_filename(filename
)
607 # Possibly remote file - search the database for a podcast
608 channel_id
= self
.db
.get_channel_id_from_episode_url(uri
)
610 if channel_id
is not None:
611 channels
= [c
for c
in self
.channels
if c
.id == channel_id
]
612 if len(channels
) == 1:
613 channel
= channels
[0]
614 return channel
.get_episode_by_url(uri
)
618 def on_played(self
, start
, end
, total
, file_uri
):
619 """Handle the "played" signal from a media player"""
620 if start
== 0 and end
== 0 and total
== 0:
621 # Ignore bogus play event
623 elif end
< start
+ 5:
624 # Ignore "less than five seconds" segments,
625 # as they can happen with seeking, etc...
628 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
629 episode
= self
.episode_object_by_uri(file_uri
)
631 if episode
is not None:
632 file_type
= episode
.file_type()
633 # Automatically enable D-Bus played status mode
634 if file_type
== 'audio':
635 self
.config
.audio_played_dbus
= True
636 elif file_type
== 'video':
637 self
.config
.video_played_dbus
= True
641 episode
.total_time
= total
643 # Assume the episode's total time for the action
644 total
= episode
.total_time
645 if episode
.current_position_updated
is None or \
646 now
> episode
.current_position_updated
:
647 episode
.current_position
= end
648 episode
.current_position_updated
= now
649 episode
.mark(is_played
=True)
652 self
.update_episode_list_icons([episode
.url
])
653 self
.update_podcast_list_model([episode
.channel
.url
])
655 # Submit this action to the webservice
656 self
.mygpo_client
.on_playback_full(episode
, \
659 def on_add_remove_podcasts_mygpo(self
):
660 actions
= self
.mygpo_client
.get_received_actions()
664 existing_urls
= [c
.url
for c
in self
.channels
]
666 # Columns for the episode selector window - just one...
668 ('description', None, None, _('Action')),
671 # A list of actions that have to be chosen from
674 # Actions that are ignored (already carried out)
677 for action
in actions
:
678 if action
.is_add
and action
.url
not in existing_urls
:
679 changes
.append(my
.Change(action
))
680 elif action
.is_remove
and action
.url
in existing_urls
:
681 podcast_object
= None
682 for podcast
in self
.channels
:
683 if podcast
.url
== action
.url
:
684 podcast_object
= podcast
686 changes
.append(my
.Change(action
, podcast_object
))
688 log('Ignoring action: %s', action
, sender
=self
)
689 ignored
.append(action
)
691 # Confirm all ignored changes
692 self
.mygpo_client
.confirm_received_actions(ignored
)
694 def execute_podcast_actions(selected
):
695 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
696 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
698 # Apply the accepted changes locally
699 self
.add_podcast_list(add_list
)
700 self
.remove_podcast_list(remove_list
, confirm
=False)
702 # All selected items are now confirmed
703 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
705 # Revert the changes on the server
706 rejected
= [c
.action
for c
in changes
if c
not in selected
]
707 self
.mygpo_client
.reject_received_actions(rejected
)
710 # We're abusing the Episode Selector again ;) -- thp
711 gPodderEpisodeSelector(self
.main_window
, \
712 title
=_('Confirm changes from gpodder.net'), \
713 instructions
=_('Select the actions you want to carry out.'), \
716 size_attribute
=None, \
717 stock_ok_button
=gtk
.STOCK_APPLY
, \
718 callback
=execute_podcast_actions
, \
721 # There are some actions that need the user's attention
726 # We have no remaining actions - no selection happens
729 def rewrite_urls_mygpo(self
):
730 # Check if we have to rewrite URLs since the last add
731 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
733 for rewritten_url
in rewritten_urls
:
734 if not rewritten_url
.new_url
:
737 for channel
in self
.channels
:
738 if channel
.url
== rewritten_url
.old_url
:
739 log('Updating URL of %s to %s', channel
, \
740 rewritten_url
.new_url
, sender
=self
)
741 channel
.url
= rewritten_url
.new_url
743 self
.channel_list_changed
= True
744 util
.idle_add(self
.update_episode_list_model
)
747 def on_send_full_subscriptions(self
):
748 # Send the full subscription list to the gpodder.net client
749 # (this will overwrite the subscription list on the server)
750 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
751 _('Your subscriptions are being uploaded to the server.'), \
752 False, self
.get_dialog_parent())
755 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
756 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
761 message
= e
.__class
__.__name
__
762 self
.show_message(message
, \
763 _('Error while uploading'), \
765 util
.idle_add(show_error
, e
)
767 util
.idle_add(indicator
.on_finished
)
769 def on_podcast_selected(self
, treeview
, path
, column
):
771 model
= treeview
.get_model()
772 channel
= model
.get_value(model
.get_iter(path
), \
773 PodcastListModel
.C_CHANNEL
)
774 self
.active_channel
= channel
775 self
.update_episode_list_model()
776 self
.episodes_window
.channel
= self
.active_channel
777 self
.episodes_window
.show()
779 def on_button_subscribe_clicked(self
, button
):
780 self
.on_itemImportChannels_activate(button
)
782 def on_button_downloads_clicked(self
, widget
):
783 self
.downloads_window
.show()
785 def show_episode_in_download_manager(self
, episode
):
786 self
.downloads_window
.show()
787 model
= self
.treeDownloads
.get_model()
788 selection
= self
.treeDownloads
.get_selection()
789 selection
.unselect_all()
790 it
= model
.get_iter_first()
791 while it
is not None:
792 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
793 if task
.episode
.url
== episode
.url
:
794 selection
.select_iter(it
)
795 # FIXME: Scroll to selection in pannable area
797 it
= model
.iter_next(it
)
799 def for_each_episode_set_task_status(self
, episodes
, status
):
800 episode_urls
= set(episode
.url
for episode
in episodes
)
801 model
= self
.treeDownloads
.get_model()
802 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
803 model
.get_value(row
.iter, \
804 DownloadStatusModel
.C_TASK
)) for row
in model \
805 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
807 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
809 def on_window_orientation_changed(self
, orientation
):
810 self
._last
_orientation
= orientation
811 if self
.preferences_dialog
is not None:
812 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
814 treeview
= self
.treeChannels
815 if orientation
== Orientation
.PORTRAIT
:
816 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
817 # Work around Maemo bug #4718
818 self
.button_subscribe
.set_name('HildonButton-thumb')
819 self
.button_refresh
.set_name('HildonButton-thumb')
821 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
822 # Work around Maemo bug #4718
823 self
.button_subscribe
.set_name('HildonButton-finger')
824 self
.button_refresh
.set_name('HildonButton-finger')
826 if gpodder
.ui
.fremantle
:
827 self
.fancy_progress_bar
.relayout()
829 def on_treeview_podcasts_selection_changed(self
, selection
):
830 model
, iter = selection
.get_selected()
832 self
.active_channel
= None
833 self
.episode_list_model
.clear()
835 def on_treeview_button_pressed(self
, treeview
, event
):
836 if event
.window
!= treeview
.get_bin_window():
839 TreeViewHelper
.save_button_press_event(treeview
, event
)
841 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
842 TreeViewHelper
.ROLE_PODCASTS
:
843 return self
.currently_updating
845 return event
.button
== self
.context_menu_mouse_button
and \
848 def on_treeview_podcasts_button_released(self
, treeview
, event
):
849 if event
.window
!= treeview
.get_bin_window():
853 return self
.treeview_channels_handle_gestures(treeview
, event
)
854 return self
.treeview_channels_show_context_menu(treeview
, event
)
856 def on_treeview_episodes_button_released(self
, treeview
, event
):
857 if event
.window
!= treeview
.get_bin_window():
860 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
861 return self
.treeview_available_handle_gestures(treeview
, event
)
863 return self
.treeview_available_show_context_menu(treeview
, event
)
865 def on_treeview_downloads_button_released(self
, treeview
, event
):
866 if event
.window
!= treeview
.get_bin_window():
869 return self
.treeview_downloads_show_context_menu(treeview
, event
)
871 def on_entry_search_podcasts_changed(self
, editable
):
872 if self
.hbox_search_podcasts
.get_property('visible'):
873 def set_search_term(self
, text
):
874 self
.podcast_list_model
.set_search_term(text
)
875 self
._podcast
_list
_search
_timeout
= None
878 if self
._podcast
_list
_search
_timeout
is not None:
879 gobject
.source_remove(self
._podcast
_list
_search
_timeout
)
880 self
._podcast
_list
_search
_timeout
= gobject
.timeout_add(\
881 self
.LIVE_SEARCH_DELAY
, \
882 set_search_term
, self
, editable
.get_chars(0, -1))
884 def on_entry_search_podcasts_key_press(self
, editable
, event
):
885 if event
.keyval
== gtk
.keysyms
.Escape
:
886 self
.hide_podcast_search()
889 def hide_podcast_search(self
, *args
):
890 if self
._podcast
_list
_search
_timeout
is not None:
891 gobject
.source_remove(self
._podcast
_list
_search
_timeout
)
892 self
._podcast
_list
_search
_timeout
= None
893 self
.hbox_search_podcasts
.hide()
894 self
.entry_search_podcasts
.set_text('')
895 self
.podcast_list_model
.set_search_term(None)
896 self
.treeChannels
.grab_focus()
898 def show_podcast_search(self
, input_char
):
899 self
.hbox_search_podcasts
.show()
900 self
.entry_search_podcasts
.insert_text(input_char
, -1)
901 self
.entry_search_podcasts
.grab_focus()
902 self
.entry_search_podcasts
.set_position(-1)
904 def init_podcast_list_treeview(self
):
905 # Set up podcast channel tree view widget
906 if gpodder
.ui
.fremantle
:
907 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
908 self
.item_view_podcasts_downloaded
.set_active(True)
909 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
910 self
.item_view_podcasts_unplayed
.set_active(True)
912 self
.item_view_podcasts_all
.set_active(True)
913 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
915 iconcolumn
= gtk
.TreeViewColumn('')
916 iconcell
= gtk
.CellRendererPixbuf()
917 iconcolumn
.pack_start(iconcell
, False)
918 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
919 self
.treeChannels
.append_column(iconcolumn
)
921 namecolumn
= gtk
.TreeViewColumn('')
922 namecell
= gtk
.CellRendererText()
923 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
924 namecolumn
.pack_start(namecell
, True)
925 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
927 if gpodder
.ui
.fremantle
:
928 countcell
= gtk
.CellRendererText()
929 from gpodder
.gtkui
.frmntl
import style
930 countcell
.set_property('font-desc', style
.get_font_desc('EmpSystemFont'))
931 countcell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
932 countcell
.set_property('alignment', pango
.ALIGN_RIGHT
)
933 countcell
.set_property('xalign', 1.)
934 countcell
.set_property('xpad', 5)
935 namecolumn
.pack_start(countcell
, False)
936 namecolumn
.add_attribute(countcell
, 'text', PodcastListModel
.C_DOWNLOADS
)
937 namecolumn
.add_attribute(countcell
, 'visible', PodcastListModel
.C_DOWNLOADS
)
939 iconcell
= gtk
.CellRendererPixbuf()
940 iconcell
.set_property('xalign', 1.0)
941 namecolumn
.pack_start(iconcell
, False)
942 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
943 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
945 self
.treeChannels
.append_column(namecolumn
)
947 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
949 # When no podcast is selected, clear the episode list model
950 selection
= self
.treeChannels
.get_selection()
951 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
953 # Set up type-ahead find for the podcast list
954 def on_key_press(treeview
, event
):
955 if event
.keyval
== gtk
.keysyms
.Escape
:
956 self
.hide_podcast_search()
957 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
958 self
.hide_podcast_search()
959 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
960 # Don't handle type-ahead when control is pressed (so shortcuts
961 # with the Ctrl key still work, e.g. Ctrl+A, ...)
964 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
965 if unicode_char_id
== 0:
967 input_char
= unichr(unicode_char_id
)
968 self
.show_podcast_search(input_char
)
970 self
.treeChannels
.connect('key-press-event', on_key_press
)
972 # Enable separators to the podcast list to separate special podcasts
973 # from others (this is used for the "all episodes" view)
974 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
976 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
978 def on_entry_search_episodes_changed(self
, editable
):
979 if self
.hbox_search_episodes
.get_property('visible'):
980 def set_search_term(self
, text
):
981 self
.episode_list_model
.set_search_term(text
)
982 self
._episode
_list
_search
_timeout
= None
985 if self
._episode
_list
_search
_timeout
is not None:
986 gobject
.source_remove(self
._episode
_list
_search
_timeout
)
987 self
._episode
_list
_search
_timeout
= gobject
.timeout_add(\
988 self
.LIVE_SEARCH_DELAY
, \
989 set_search_term
, self
, editable
.get_chars(0, -1))
991 def on_entry_search_episodes_key_press(self
, editable
, event
):
992 if event
.keyval
== gtk
.keysyms
.Escape
:
993 self
.hide_episode_search()
996 def hide_episode_search(self
, *args
):
997 if self
._episode
_list
_search
_timeout
is not None:
998 gobject
.source_remove(self
._episode
_list
_search
_timeout
)
999 self
._episode
_list
_search
_timeout
= None
1000 self
.hbox_search_episodes
.hide()
1001 self
.entry_search_episodes
.set_text('')
1002 self
.episode_list_model
.set_search_term(None)
1003 self
.treeAvailable
.grab_focus()
1005 def show_episode_search(self
, input_char
):
1006 self
.hbox_search_episodes
.show()
1007 self
.entry_search_episodes
.insert_text(input_char
, -1)
1008 self
.entry_search_episodes
.grab_focus()
1009 self
.entry_search_episodes
.set_position(-1)
1011 def init_episode_list_treeview(self
):
1012 # For loading the list model
1013 self
.episode_list_model
= EpisodeListModel(self
.on_episode_list_filter_changed
)
1015 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
1016 self
.item_view_episodes_undeleted
.set_active(True)
1017 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
1018 self
.item_view_episodes_downloaded
.set_active(True)
1019 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
1020 self
.item_view_episodes_unplayed
.set_active(True)
1022 self
.item_view_episodes_all
.set_active(True)
1024 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
1026 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
1028 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
1030 iconcell
= gtk
.CellRendererPixbuf()
1031 iconcell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1032 if gpodder
.ui
.maemo
:
1033 iconcell
.set_fixed_size(50, 50)
1035 iconcell
.set_fixed_size(40, -1)
1037 namecell
= gtk
.CellRendererText()
1038 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1039 namecolumn
= gtk
.TreeViewColumn(_('Episode'))
1040 namecolumn
.pack_start(iconcell
, False)
1041 namecolumn
.add_attribute(iconcell
, 'icon-name', EpisodeListModel
.C_STATUS_ICON
)
1042 namecolumn
.pack_start(namecell
, True)
1043 namecolumn
.add_attribute(namecell
, 'markup', EpisodeListModel
.C_DESCRIPTION
)
1044 if gpodder
.ui
.fremantle
:
1045 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_FIXED
)
1047 namecolumn
.set_sort_column_id(EpisodeListModel
.C_DESCRIPTION
)
1048 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1049 namecolumn
.set_resizable(True)
1050 namecolumn
.set_expand(True)
1052 if gpodder
.ui
.fremantle
:
1053 from gpodder
.gtkui
.frmntl
import style
1054 timecell
= gtk
.CellRendererText()
1055 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1056 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1057 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1058 timecell
.set_property('xalign', 1.)
1059 timecell
.set_property('xpad', 5)
1060 namecolumn
.pack_start(timecell
, False)
1061 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1062 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME1_VISIBLE
)
1064 # Add another cell renderer to fix a sizing issue (one renderer
1065 # only renders short text and the other one longer text to avoid
1066 # having titles of episodes unnecessarily cut off)
1067 timecell
= gtk
.CellRendererText()
1068 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1069 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1070 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1071 timecell
.set_property('xalign', 1.)
1072 timecell
.set_property('xpad', 5)
1073 namecolumn
.pack_start(timecell
, False)
1074 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1075 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME2_VISIBLE
)
1077 lockcell
= gtk
.CellRendererPixbuf()
1078 lockcell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1079 if gpodder
.ui
.fremantle
:
1080 lockcell
.set_property('icon-name', 'general_locked')
1082 lockcell
.set_property('icon-name', 'emblem-readonly')
1084 namecolumn
.pack_start(lockcell
, False)
1085 namecolumn
.add_attribute(lockcell
, 'visible', EpisodeListModel
.C_LOCKED
)
1087 sizecell
= gtk
.CellRendererText()
1088 sizecell
.set_property('xalign', 1)
1089 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
1090 sizecolumn
.set_sort_column_id(EpisodeListModel
.C_FILESIZE
)
1092 releasecell
= gtk
.CellRendererText()
1093 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
1094 releasecolumn
.set_sort_column_id(EpisodeListModel
.C_PUBLISHED
)
1096 namecolumn
.set_reorderable(True)
1097 self
.treeAvailable
.append_column(namecolumn
)
1099 if not gpodder
.ui
.maemo
:
1100 for itemcolumn
in (sizecolumn
, releasecolumn
):
1101 itemcolumn
.set_reorderable(True)
1102 self
.treeAvailable
.append_column(itemcolumn
)
1104 # Set up type-ahead find for the episode list
1105 def on_key_press(treeview
, event
):
1106 if event
.keyval
== gtk
.keysyms
.Escape
:
1107 self
.hide_episode_search()
1108 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
1109 self
.hide_episode_search()
1110 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
1111 # Don't handle type-ahead when control is pressed (so shortcuts
1112 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1115 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1116 if unicode_char_id
== 0:
1118 input_char
= unichr(unicode_char_id
)
1119 self
.show_episode_search(input_char
)
1121 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1123 if gpodder
.ui
.desktop
and not self
.config
.enable_fingerscroll
:
1124 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1125 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1126 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1127 if self
.config
.on_drag_mark_played
:
1128 for episode
in self
.get_selected_episodes():
1129 episode
.mark(is_played
=True)
1130 self
.on_selected_episodes_status_changed()
1131 uris
= ['file://'+e
.local_filename(create
=False) \
1132 for e
in self
.get_selected_episodes() \
1133 if e
.was_downloaded(and_exists
=True)]
1134 uris
.append('') # for the trailing '\r\n'
1135 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1136 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1138 selection
= self
.treeAvailable
.get_selection()
1139 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
1140 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1141 elif gpodder
.ui
.fremantle
:
1142 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1144 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1145 # Update the sensitivity of the toolbar buttons on the Desktop
1146 selection
.connect('changed', lambda s
: self
.play_or_download())
1148 if gpodder
.ui
.diablo
:
1149 # Set up the tap-and-hold context menu for podcasts
1151 menu
.append(self
.itemUpdateChannel
.create_menu_item())
1152 menu
.append(self
.itemEditChannel
.create_menu_item())
1153 menu
.append(gtk
.SeparatorMenuItem())
1154 menu
.append(self
.itemRemoveChannel
.create_menu_item())
1155 menu
.append(gtk
.SeparatorMenuItem())
1156 item
= gtk
.ImageMenuItem(_('Close this menu'))
1157 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1158 gtk
.ICON_SIZE_MENU
))
1161 menu
= self
.set_finger_friendly(menu
)
1162 self
.treeChannels
.tap_and_hold_setup(menu
)
1165 def init_download_list_treeview(self
):
1166 # enable multiple selection support
1167 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1168 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1170 # columns and renderers for "download progress" tab
1171 # First column: [ICON] Episodename
1172 column
= gtk
.TreeViewColumn(_('Episode'))
1174 cell
= gtk
.CellRendererPixbuf()
1175 if gpodder
.ui
.maemo
:
1176 cell
.set_fixed_size(50, 50)
1177 cell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1178 column
.pack_start(cell
, expand
=False)
1179 column
.add_attribute(cell
, 'icon-name', \
1180 DownloadStatusModel
.C_ICON_NAME
)
1182 cell
= gtk
.CellRendererText()
1183 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1184 column
.pack_start(cell
, expand
=True)
1185 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1186 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1187 column
.set_expand(True)
1188 self
.treeDownloads
.append_column(column
)
1190 # Second column: Progress
1191 cell
= gtk
.CellRendererProgress()
1192 cell
.set_property('yalign', .5)
1193 cell
.set_property('ypad', 6)
1194 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1195 value
=DownloadStatusModel
.C_PROGRESS
, \
1196 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1197 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1198 column
.set_expand(False)
1199 self
.treeDownloads
.append_column(column
)
1200 if gpodder
.ui
.maemo
:
1201 column
.set_property('min-width', 200)
1202 column
.set_property('max-width', 200)
1204 column
.set_property('min-width', 150)
1205 column
.set_property('max-width', 150)
1207 self
.treeDownloads
.set_model(self
.download_status_model
)
1208 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1210 def on_treeview_expose_event(self
, treeview
, event
):
1211 if event
.window
== treeview
.get_bin_window():
1212 model
= treeview
.get_model()
1213 if (model
is not None and model
.get_iter_first() is not None):
1216 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1220 ctx
= event
.window
.cairo_create()
1221 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1222 event
.area
.width
, event
.area
.height
)
1225 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1228 if role
== TreeViewHelper
.ROLE_EPISODES
:
1229 if self
.currently_updating
:
1230 text
= _('Loading episodes')
1231 elif self
.config
.episode_list_view_mode
!= \
1232 EpisodeListModel
.VIEW_ALL
:
1233 text
= _('No episodes in current view')
1235 text
= _('No episodes available')
1236 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1237 if self
.config
.episode_list_view_mode
!= \
1238 EpisodeListModel
.VIEW_ALL
and \
1239 self
.config
.podcast_list_hide_boring
and \
1240 len(self
.channels
) > 0:
1241 text
= _('No podcasts in this view')
1243 text
= _('No subscriptions')
1244 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1245 text
= _('No active downloads')
1247 raise Exception('on_treeview_expose_event: unknown role')
1249 if gpodder
.ui
.fremantle
:
1250 from gpodder
.gtkui
.frmntl
import style
1251 font_desc
= style
.get_font_desc('LargeSystemFont')
1255 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1259 def enable_download_list_update(self
):
1260 if not self
.download_list_update_enabled
:
1261 self
.update_downloads_list()
1262 gobject
.timeout_add(1500, self
.update_downloads_list
)
1263 self
.download_list_update_enabled
= True
1265 def cleanup_downloads(self
):
1266 model
= self
.download_status_model
1268 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1269 changed_episode_urls
= set()
1270 for row_reference
, task
in all_tasks
:
1271 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1272 model
.remove(model
.get_iter(row_reference
.get_path()))
1274 # We don't "see" this task anymore - remove it;
1275 # this is needed, so update_episode_list_icons()
1276 # below gets the correct list of "seen" tasks
1277 self
.download_tasks_seen
.remove(task
)
1278 except KeyError, key_error
:
1279 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1280 changed_episode_urls
.add(task
.url
)
1281 # Tell the task that it has been removed (so it can clean up)
1282 task
.removed_from_list()
1284 # Tell the podcasts tab to update icons for our removed podcasts
1285 self
.update_episode_list_icons(changed_episode_urls
)
1287 # Tell the shownotes window that we have removed the episode
1288 if self
.episode_shownotes_window
is not None and \
1289 self
.episode_shownotes_window
.episode
is not None and \
1290 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1291 self
.episode_shownotes_window
._download
_status
_changed
(None)
1293 # Update the downloads list one more time
1294 self
.update_downloads_list(can_call_cleanup
=False)
1296 def on_tool_downloads_toggled(self
, toolbutton
):
1297 if toolbutton
.get_active():
1298 self
.wNotebook
.set_current_page(1)
1300 self
.wNotebook
.set_current_page(0)
1302 def add_download_task_monitor(self
, monitor
):
1303 self
.download_task_monitors
.add(monitor
)
1304 model
= self
.download_status_model
1308 task
= row
[self
.download_status_model
.C_TASK
]
1309 monitor
.task_updated(task
)
1311 def remove_download_task_monitor(self
, monitor
):
1312 self
.download_task_monitors
.remove(monitor
)
1314 def update_downloads_list(self
, can_call_cleanup
=True):
1316 model
= self
.download_status_model
1318 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1319 total_speed
, total_size
, done_size
= 0, 0, 0
1321 # Keep a list of all download tasks that we've seen
1322 download_tasks_seen
= set()
1324 # Remember the DownloadTask object for the episode that
1325 # has been opened in the episode shownotes dialog (if any)
1326 if self
.episode_shownotes_window
is not None:
1327 shownotes_episode
= self
.episode_shownotes_window
.episode
1328 shownotes_task
= None
1330 shownotes_episode
= None
1331 shownotes_task
= None
1333 # Do not go through the list of the model is not (yet) available
1337 failed_downloads
= []
1339 self
.download_status_model
.request_update(row
.iter)
1341 task
= row
[self
.download_status_model
.C_TASK
]
1342 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1344 # Let the download task monitors know of changes
1345 for monitor
in self
.download_task_monitors
:
1346 monitor
.task_updated(task
)
1349 done_size
+= size
*progress
1351 if shownotes_episode
is not None and \
1352 shownotes_episode
.url
== task
.episode
.url
:
1353 shownotes_task
= task
1355 download_tasks_seen
.add(task
)
1357 if status
== download
.DownloadTask
.DOWNLOADING
:
1359 total_speed
+= speed
1360 elif status
== download
.DownloadTask
.FAILED
:
1361 failed_downloads
.append(task
)
1363 elif status
== download
.DownloadTask
.DONE
:
1365 elif status
== download
.DownloadTask
.QUEUED
:
1367 elif status
== download
.DownloadTask
.PAUSED
:
1372 # Remember which tasks we have seen after this run
1373 self
.download_tasks_seen
= download_tasks_seen
1375 if gpodder
.ui
.desktop
:
1376 text
= [_('Downloads')]
1377 if downloading
+ failed
+ queued
> 0:
1380 s
.append(N_('%(count)d active', '%(count)d active', downloading
) % {'count':downloading
})
1382 s
.append(N_('%(count)d failed', '%(count)d failed', failed
) % {'count':failed
})
1384 s
.append(N_('%(count)d queued', '%(count)d queued', queued
) % {'count':queued
})
1385 text
.append(' (' + ', '.join(s
)+')')
1386 self
.labelDownloads
.set_text(''.join(text
))
1387 elif gpodder
.ui
.diablo
:
1388 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1390 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1392 self
.tool_downloads
.set_label(_('Downloads'))
1393 elif gpodder
.ui
.fremantle
:
1394 if downloading
+ queued
> 0:
1395 self
.button_downloads
.set_value(N_('%(count)d active', '%(count)d active', downloading
+queued
) % {'count':(downloading
+queued
)})
1397 self
.button_downloads
.set_value(N_('%(count)d failed', '%(count)d failed', failed
) % {'count':failed
})
1399 self
.button_downloads
.set_value(N_('%(count)d paused', '%(count)d paused', paused
) % {'count':paused
})
1401 self
.button_downloads
.set_value(_('Idle'))
1403 title
= [self
.default_title
]
1405 # We have to update all episodes/channels for which the status has
1406 # changed. Accessing task.status_changed has the side effect of
1407 # re-setting the changed flag, so we need to get the "changed" list
1408 # of tuples first and split it into two lists afterwards
1409 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1410 self
.download_tasks_seen
if task
.status_changed
]
1411 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1412 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1414 count
= downloading
+ queued
1416 title
.append(N_('downloading %(count)d file', 'downloading %(count)d files', count
) % {'count':count
})
1419 percentage
= 100.0*done_size
/total_size
1422 total_speed
= util
.format_filesize(total_speed
)
1423 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1424 if self
.tray_icon
is not None:
1425 # Update the tray icon status and progress bar
1426 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1427 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1429 if self
.tray_icon
is not None:
1430 # Update the tray icon status
1431 self
.tray_icon
.set_status()
1432 if gpodder
.ui
.desktop
:
1433 self
.downloads_finished(self
.download_tasks_seen
)
1434 if gpodder
.ui
.diablo
:
1435 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1436 log('All downloads have finished.', sender
=self
)
1437 if self
.config
.cmd_all_downloads_complete
:
1438 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1440 if gpodder
.ui
.fremantle
and failed
:
1441 message
= '\n'.join(['%s: %s' % (str(task
), \
1442 task
.error_message
) for task
in failed_downloads
])
1443 self
.show_message(message
, _('Downloads failed'), important
=True)
1445 # Remove finished episodes
1446 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1447 self
.cleanup_downloads()
1449 # Stop updating the download list here
1450 self
.download_list_update_enabled
= False
1452 if not gpodder
.ui
.fremantle
:
1453 self
.gPodder
.set_title(' - '.join(title
))
1455 self
.update_episode_list_icons(episode_urls
)
1456 if self
.episode_shownotes_window
is not None:
1457 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1458 shownotes_task
!= self
.episode_shownotes_window
.task
:
1459 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1460 self
.episode_shownotes_window
._download
_status
_progress
()
1461 self
.play_or_download()
1463 self
.update_podcast_list_model(channel_urls
)
1465 return self
.download_list_update_enabled
1466 except Exception, e
:
1467 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1468 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1469 # We return False here, so the update loop won't be called again,
1470 # that's why we require the restart of gPodder in the message.
1473 def on_config_changed(self
, *args
):
1474 util
.idle_add(self
._on
_config
_changed
, *args
)
1476 def _on_config_changed(self
, name
, old_value
, new_value
):
1477 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1478 self
.toolbar
.set_property('visible', new_value
)
1479 elif name
== 'videoplayer':
1480 self
.config
.video_played_dbus
= False
1481 elif name
== 'player':
1482 self
.config
.audio_played_dbus
= False
1483 elif name
== 'episode_list_descriptions':
1484 self
.update_episode_list_model()
1485 elif name
== 'episode_list_thumbnails':
1486 self
.update_episode_list_icons(all
=True)
1487 elif name
== 'rotation_mode':
1488 self
._fremantle
_rotation
.set_mode(new_value
)
1489 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1490 self
.restart_auto_update_timer()
1491 elif name
== 'podcast_list_view_all':
1492 # Force a update of the podcast list model
1493 self
.channel_list_changed
= True
1494 if gpodder
.ui
.fremantle
:
1495 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1496 while gtk
.events_pending():
1497 gtk
.main_iteration(False)
1498 self
.update_podcast_list_model()
1499 if gpodder
.ui
.fremantle
:
1500 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1502 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1503 # With get_bin_window, we get the window that contains the rows without
1504 # the header. The Y coordinate of this window will be the height of the
1505 # treeview header. This is the amount we have to subtract from the
1506 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1507 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1510 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1512 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or x
> 50 or (column
is not None and column
!= treeview
.get_columns()[0]):
1513 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1516 if path
is not None:
1517 model
= treeview
.get_model()
1518 iter = model
.get_iter(path
)
1519 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1521 if role
== TreeViewHelper
.ROLE_EPISODES
:
1522 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1523 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1524 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1526 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1527 if last_tooltip
is not None and last_tooltip
!= id:
1528 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1530 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1532 if role
== TreeViewHelper
.ROLE_EPISODES
:
1533 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1535 tooltip
.set_text(description
)
1538 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1539 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1542 channel
.request_save_dir_size()
1543 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1544 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1546 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1547 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1548 table
= gtk
.Table(rows
=3, columns
=3)
1549 table
.set_row_spacings(5)
1550 table
.set_col_spacings(5)
1551 table
.set_border_width(5)
1553 heading
= gtk
.Label()
1554 heading
.set_alignment(0, 1)
1555 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1556 table
.attach(heading
, 0, 1, 0, 1)
1557 size_info
= gtk
.Label()
1558 size_info
.set_alignment(1, 1)
1559 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1560 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1561 table
.attach(size_info
, 2, 3, 0, 1)
1563 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1565 if len(channel
.description
) < 500:
1566 description
= channel
.description
1568 pos
= channel
.description
.find('\n\n')
1569 if pos
== -1 or pos
> 500:
1570 description
= channel
.description
[:498]+'[...]'
1572 description
= channel
.description
[:pos
]
1574 description
= gtk
.Label(description
)
1576 description
.set_markup(error_str
)
1577 description
.set_alignment(0, 0)
1578 description
.set_line_wrap(True)
1579 table
.attach(description
, 0, 3, 2, 3)
1582 tooltip
.set_custom(table
)
1586 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1589 def treeview_allow_tooltips(self
, treeview
, allow
):
1590 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1592 def update_m3u_playlist_clicked(self
, widget
):
1593 if self
.active_channel
is not None:
1594 self
.active_channel
.update_m3u_playlist()
1595 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1597 def treeview_handle_context_menu_click(self
, treeview
, event
):
1598 x
, y
= int(event
.x
), int(event
.y
)
1599 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1601 selection
= treeview
.get_selection()
1602 model
, paths
= selection
.get_selected_rows()
1604 if path
is None or (path
not in paths
and \
1605 event
.button
== self
.context_menu_mouse_button
):
1606 # We have right-clicked, but not into the selection,
1607 # assume we don't want to operate on the selection
1610 if path
is not None and not paths
and \
1611 event
.button
== self
.context_menu_mouse_button
:
1612 # No selection or clicked outside selection;
1613 # select the single item where we clicked
1614 treeview
.grab_focus()
1615 treeview
.set_cursor(path
, column
, 0)
1619 # Unselect any remaining items (clicked elsewhere)
1620 if hasattr(treeview
, 'is_rubber_banding_active'):
1621 if not treeview
.is_rubber_banding_active():
1622 selection
.unselect_all()
1624 selection
.unselect_all()
1628 def downloads_list_get_selection(self
, model
=None, paths
=None):
1629 if model
is None and paths
is None:
1630 selection
= self
.treeDownloads
.get_selection()
1631 model
, paths
= selection
.get_selected_rows()
1633 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1634 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1635 model
.get_value(model
.get_iter(path
), \
1636 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1638 for row_reference
, task
in selected_tasks
:
1639 if task
.status
!= download
.DownloadTask
.QUEUED
:
1641 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1642 download
.DownloadTask
.FAILED
, \
1643 download
.DownloadTask
.CANCELLED
):
1645 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1646 download
.DownloadTask
.QUEUED
, \
1647 download
.DownloadTask
.DOWNLOADING
, \
1648 download
.DownloadTask
.FAILED
):
1650 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1651 download
.DownloadTask
.DOWNLOADING
):
1653 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1654 download
.DownloadTask
.FAILED
, \
1655 download
.DownloadTask
.DONE
):
1658 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1660 def downloads_finished(self
, download_tasks_seen
):
1661 # FIXME: Filter all tasks that have already been reported
1662 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1663 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1665 if finished_downloads
and failed_downloads
:
1666 message
= self
.format_episode_list(finished_downloads
, 5)
1667 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1668 message
+= self
.format_episode_list(failed_downloads
, 5)
1669 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1670 elif finished_downloads
:
1671 message
= self
.format_episode_list(finished_downloads
)
1672 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1673 elif failed_downloads
:
1674 message
= self
.format_episode_list(failed_downloads
)
1675 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1677 # Open torrent files right after download (bug 1029)
1678 if self
.config
.open_torrent_after_download
:
1679 for task
in download_tasks_seen
:
1680 if task
.status
!= task
.DONE
:
1683 episode
= task
.episode
1684 if episode
.mimetype
!= 'application/x-bittorrent':
1687 self
.playback_episodes([episode
])
1690 def format_episode_list(self
, episode_list
, max_episodes
=10):
1692 Format a list of episode names for notifications
1694 Will truncate long episode names and limit the amount of
1695 episodes displayed (max_episodes=10).
1697 The episode_list parameter should be a list of strings.
1699 MAX_TITLE_LENGTH
= 100
1702 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1703 if len(title
) > MAX_TITLE_LENGTH
:
1704 middle
= (MAX_TITLE_LENGTH
/2)-2
1705 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1706 result
.append(saxutils
.escape(title
))
1709 more_episodes
= len(episode_list
) - max_episodes
1710 if more_episodes
> 0:
1711 result
.append('(...')
1712 result
.append(N_('%(count)d more episode', '%(count)d more episodes', more_episodes
) % {'count':more_episodes
})
1713 result
.append('...)')
1715 return (''.join(result
)).strip()
1717 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1718 episode_urls
= set()
1719 model
= self
.treeDownloads
.get_model()
1720 for row_reference
, task
in tasks
:
1721 if status
== download
.DownloadTask
.QUEUED
:
1722 # Only queue task when its paused/failed/cancelled (or forced)
1723 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1724 self
.download_queue_manager
.add_task(task
, force_start
)
1725 self
.enable_download_list_update()
1726 elif status
== download
.DownloadTask
.CANCELLED
:
1727 # Cancelling a download allowed when downloading/queued
1728 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1729 task
.status
= status
1730 # Cancelling paused/failed downloads requires a call to .run()
1731 elif task
.status
in (task
.PAUSED
, task
.FAILED
):
1732 task
.status
= status
1733 # Call run, so the partial file gets deleted
1735 elif status
== download
.DownloadTask
.PAUSED
:
1736 # Pausing a download only when queued/downloading
1737 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1738 task
.status
= status
1739 elif status
is None:
1740 # Remove the selected task - cancel downloading/queued tasks
1741 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1742 task
.status
= task
.CANCELLED
1743 model
.remove(model
.get_iter(row_reference
.get_path()))
1744 # Remember the URL, so we can tell the UI to update
1746 # We don't "see" this task anymore - remove it;
1747 # this is needed, so update_episode_list_icons()
1748 # below gets the correct list of "seen" tasks
1749 self
.download_tasks_seen
.remove(task
)
1750 except KeyError, key_error
:
1751 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1752 episode_urls
.add(task
.url
)
1753 # Tell the task that it has been removed (so it can clean up)
1754 task
.removed_from_list()
1756 # We can (hopefully) simply set the task status here
1757 task
.status
= status
1758 # Tell the podcasts tab to update icons for our removed podcasts
1759 self
.update_episode_list_icons(episode_urls
)
1760 # Update the tab title and downloads list
1761 self
.update_downloads_list()
1763 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1764 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1766 if not hasattr(treeview
, 'is_rubber_banding_active'):
1769 return not treeview
.is_rubber_banding_active()
1771 if event
.button
== self
.context_menu_mouse_button
:
1772 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1773 self
.downloads_list_get_selection(model
, paths
)
1775 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1776 # This creates a menu item for selection-wide actions
1777 item
= gtk
.ImageMenuItem(label
)
1778 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1779 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1780 item
.set_sensitive(sensitive
)
1781 return self
.set_finger_friendly(item
)
1785 item
= gtk
.ImageMenuItem(_('Episode details'))
1786 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1787 if len(selected_tasks
) == 1:
1788 row_reference
, task
= selected_tasks
[0]
1789 episode
= task
.episode
1790 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1792 item
.set_sensitive(False)
1793 menu
.append(self
.set_finger_friendly(item
))
1794 menu
.append(gtk
.SeparatorMenuItem())
1796 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1798 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1799 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1800 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1801 menu
.append(gtk
.SeparatorMenuItem())
1802 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1804 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
1805 # Because we open the popup on left-click for Maemo,
1806 # we also include a non-action to close the menu
1807 menu
.append(gtk
.SeparatorMenuItem())
1808 item
= gtk
.ImageMenuItem(_('Close this menu'))
1809 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1811 menu
.append(self
.set_finger_friendly(item
))
1814 menu
.popup(None, None, None, event
.button
, event
.time
)
1817 def treeview_channels_show_context_menu(self
, treeview
, event
):
1818 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1822 # Check for valid channel id, if there's no id then
1823 # assume that it is a proxy channel or equivalent
1824 # and cannot be operated with right click
1825 if self
.active_channel
.id is None:
1828 if event
.button
== 3:
1833 item
= gtk
.ImageMenuItem( _('Update podcast'))
1834 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1835 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1836 item
.set_sensitive(not self
.updating_feed_cache
)
1839 menu
.append(gtk
.SeparatorMenuItem())
1841 item
= gtk
.CheckMenuItem(_('Keep episodes'))
1842 item
.set_active(self
.active_channel
.channel_is_locked
)
1843 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1844 menu
.append(self
.set_finger_friendly(item
))
1846 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1847 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1848 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1851 if self
.config
.device_type
!= 'none':
1852 item
= gtk
.MenuItem(_('Synchronize to device'))
1853 item
.connect('activate', lambda item
: self
.on_sync_to_ipod_activate(item
, self
.active_channel
.get_downloaded_episodes()))
1856 menu
.append( gtk
.SeparatorMenuItem())
1858 item
= gtk
.ImageMenuItem(_('Podcast details'))
1859 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1860 item
.connect('activate', self
.on_itemEditChannel_activate
)
1864 # Disable tooltips while we are showing the menu, so
1865 # the tooltip will not appear over the menu
1866 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1867 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1868 menu
.popup( None, None, None, event
.button
, event
.time
)
1872 def on_itemClose_activate(self
, widget
):
1873 if self
.tray_icon
is not None:
1874 self
.iconify_main_window()
1876 self
.on_gPodder_delete_event(widget
)
1878 def cover_file_removed(self
, channel_url
):
1880 The Cover Downloader calls this when a previously-
1881 available cover has been removed from the disk. We
1882 have to update our model to reflect this change.
1884 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1886 def cover_download_finished(self
, channel
, pixbuf
):
1888 The Cover Downloader calls this when it has finished
1889 downloading (or registering, if already downloaded)
1890 a new channel cover, which is ready for displaying.
1892 self
.podcast_list_model
.add_cover_by_channel(channel
, pixbuf
)
1894 def save_episodes_as_file(self
, episodes
):
1895 for episode
in episodes
:
1896 self
.save_episode_as_file(episode
)
1898 def save_episode_as_file(self
, episode
):
1899 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1900 if episode
.was_downloaded(and_exists
=True):
1901 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1902 copy_from
= episode
.local_filename(create
=False)
1903 assert copy_from
is not None
1904 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1905 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1906 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1908 def copy_episodes_bluetooth(self
, episodes
):
1909 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1911 if gpodder
.ui
.maemo
:
1912 util
.bluetooth_send_files_maemo([e
.local_filename(create
=False) \
1913 for e
in episodes_to_copy
])
1916 def convert_and_send_thread(episode
):
1917 for episode
in episodes
:
1918 filename
= episode
.local_filename(create
=False)
1919 assert filename
is not None
1920 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1921 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1922 (base
, ext
) = os
.path
.splitext(filename
)
1923 if not destfile
.endswith(ext
):
1927 shutil
.copyfile(filename
, destfile
)
1928 util
.bluetooth_send_file(destfile
)
1930 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1931 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1933 util
.delete_file(destfile
)
1935 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1937 def get_device_name(self
):
1938 if self
.config
.device_type
== 'ipod':
1940 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1941 return _('MP3 player')
1943 return '(unknown device)'
1945 def _treeview_button_released(self
, treeview
, event
):
1946 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1947 dy
= int(abs(event
.y
-ypos
))
1948 dx
= int(event
.x
-xpos
)
1950 selection
= treeview
.get_selection()
1951 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1952 if path
is None or dy
> 30:
1953 return (False, dx
, dy
)
1955 path
, column
, x
, y
= path
1956 selection
.select_path(path
)
1957 treeview
.set_cursor(path
)
1958 treeview
.grab_focus()
1960 return (True, dx
, dy
)
1962 def treeview_channels_handle_gestures(self
, treeview
, event
):
1963 if self
.currently_updating
:
1966 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1969 if self
.config
.maemo_enable_gestures
:
1971 self
.on_itemUpdateChannel_activate()
1973 self
.on_itemEditChannel_activate(treeview
)
1977 def treeview_available_handle_gestures(self
, treeview
, event
):
1978 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1981 if self
.config
.maemo_enable_gestures
:
1983 self
.on_playback_selected_episodes(None)
1986 self
.on_shownotes_selected_episodes(None)
1989 # Pass the event to the context menu handler for treeAvailable
1990 self
.treeview_available_show_context_menu(treeview
, event
)
1994 def treeview_available_show_context_menu(self
, treeview
, event
):
1995 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1997 if not hasattr(treeview
, 'is_rubber_banding_active'):
2000 return not treeview
.is_rubber_banding_active()
2002 if event
.button
== self
.context_menu_mouse_button
:
2003 episodes
= self
.get_selected_episodes()
2004 any_locked
= any(e
.is_locked
for e
in episodes
)
2005 any_played
= any(e
.is_played
for e
in episodes
)
2006 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
2007 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
2008 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
2012 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
2014 if open_instead_of_play
:
2015 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
2017 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
2019 item
= gtk
.ImageMenuItem(_('Stream'))
2020 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
2022 item
.set_sensitive(can_play
and not downloading
)
2023 item
.connect('activate', self
.on_playback_selected_episodes
)
2024 menu
.append(self
.set_finger_friendly(item
))
2027 item
= gtk
.ImageMenuItem(_('Download'))
2028 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
2029 item
.set_sensitive(can_download
)
2030 item
.connect('activate', self
.on_download_selected_episodes
)
2031 menu
.append(self
.set_finger_friendly(item
))
2033 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
2034 item
.connect('activate', self
.on_item_cancel_download_activate
)
2035 menu
.append(self
.set_finger_friendly(item
))
2037 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
2038 item
.set_sensitive(can_delete
)
2039 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
2040 menu
.append(self
.set_finger_friendly(item
))
2044 # Ok, this probably makes sense to only display for downloaded files
2046 menu
.append(gtk
.SeparatorMenuItem())
2047 share_item
= gtk
.MenuItem(_('Send to'))
2048 menu
.append(self
.set_finger_friendly(share_item
))
2049 share_menu
= gtk
.Menu()
2051 item
= gtk
.ImageMenuItem(_('Local folder'))
2052 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
2053 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
2054 share_menu
.append(self
.set_finger_friendly(item
))
2055 if self
.bluetooth_available
:
2056 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
2057 if gpodder
.ui
.maemo
:
2058 icon_name
= ICON('qgn_list_filesys_bluetooth')
2060 icon_name
= ICON('bluetooth')
2061 item
.set_image(gtk
.image_new_from_icon_name(icon_name
, gtk
.ICON_SIZE_MENU
))
2062 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
2063 share_menu
.append(self
.set_finger_friendly(item
))
2065 item
= gtk
.ImageMenuItem(self
.get_device_name())
2066 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
2067 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
2068 share_menu
.append(self
.set_finger_friendly(item
))
2070 share_item
.set_submenu(share_menu
)
2072 if (downloaded
or one_is_new
or can_download
) and not downloading
:
2073 menu
.append(gtk
.SeparatorMenuItem())
2075 item
= gtk
.CheckMenuItem(_('New'))
2076 item
.set_active(True)
2077 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
2078 menu
.append(self
.set_finger_friendly(item
))
2080 item
= gtk
.CheckMenuItem(_('New'))
2081 item
.set_active(False)
2082 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
2083 menu
.append(self
.set_finger_friendly(item
))
2086 item
= gtk
.CheckMenuItem(_('Played'))
2087 item
.set_active(any_played
)
2088 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
2089 menu
.append(self
.set_finger_friendly(item
))
2091 item
= gtk
.CheckMenuItem(_('Keep episode'))
2092 item
.set_active(any_locked
)
2093 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
2094 menu
.append(self
.set_finger_friendly(item
))
2096 menu
.append(gtk
.SeparatorMenuItem())
2097 # Single item, add episode information menu item
2098 item
= gtk
.ImageMenuItem(_('Episode details'))
2099 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
2100 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
2101 menu
.append(self
.set_finger_friendly(item
))
2103 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
2104 # Because we open the popup on left-click for Maemo,
2105 # we also include a non-action to close the menu
2106 menu
.append(gtk
.SeparatorMenuItem())
2107 item
= gtk
.ImageMenuItem(_('Close this menu'))
2108 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
2109 menu
.append(self
.set_finger_friendly(item
))
2112 # Disable tooltips while we are showing the menu, so
2113 # the tooltip will not appear over the menu
2114 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
2115 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2116 menu
.popup( None, None, None, event
.button
, event
.time
)
2120 def set_title(self
, new_title
):
2121 if not gpodder
.ui
.fremantle
:
2122 self
.default_title
= new_title
2123 self
.gPodder
.set_title(new_title
)
2125 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2127 Updates the status icons in the episode list.
2129 If urls is given, it should be a list of URLs
2130 of episodes that should be updated.
2132 If urls is None, set ONE OF selected, all to
2133 True (the former updates just the selected
2134 episodes and the latter updates all episodes).
2136 additional_args
= (self
.episode_is_downloading
, \
2137 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2138 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2140 if urls
is not None:
2141 # We have a list of URLs to walk through
2142 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2143 elif selected
and not all
:
2144 # We should update all selected episodes
2145 selection
= self
.treeAvailable
.get_selection()
2146 model
, paths
= selection
.get_selected_rows()
2147 for path
in reversed(paths
):
2148 iter = model
.get_iter(path
)
2149 self
.episode_list_model
.update_by_filter_iter(iter, \
2151 elif all
and not selected
:
2152 # We update all (even the filter-hidden) episodes
2153 self
.episode_list_model
.update_all(*additional_args
)
2155 # Wrong/invalid call - have to specify at least one parameter
2156 raise ValueError('Invalid call to update_episode_list_icons')
2158 def episode_list_status_changed(self
, episodes
):
2159 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2160 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2163 def clean_up_downloads(self
, delete_partial
=False):
2164 # Clean up temporary files left behind by old gPodder versions
2165 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2168 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2170 for tempfile
in temporary_files
:
2171 util
.delete_file(tempfile
)
2173 # Clean up empty download folders and abandoned download folders
2174 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2175 for ddir
in download_dirs
:
2176 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2177 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2178 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2179 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2180 shutil
.rmtree(ddir
, ignore_errors
=True)
2182 def streaming_possible(self
):
2183 if gpodder
.ui
.desktop
:
2184 # User has to have a media player set on the Desktop, or else we
2185 # would probably open the browser when giving a URL to xdg-open..
2186 return (self
.config
.player
and self
.config
.player
!= 'default')
2187 elif gpodder
.ui
.maemo
:
2188 # On Maemo, the default is to use the Nokia Media Player, which is
2189 # already able to deal with HTTP URLs the right way, so we
2190 # unconditionally enable streaming always on Maemo
2195 def playback_episodes_for_real(self
, episodes
):
2196 groups
= collections
.defaultdict(list)
2197 for episode
in episodes
:
2198 file_type
= episode
.file_type()
2199 if file_type
== 'video' and self
.config
.videoplayer
and \
2200 self
.config
.videoplayer
!= 'default':
2201 player
= self
.config
.videoplayer
2202 if gpodder
.ui
.diablo
:
2203 # Use the wrapper script if it's installed to crop 3GP YouTube
2204 # videos to fit the screen (looks much nicer than w/ black border)
2205 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2206 player
= 'gpodder-mplayer'
2207 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2208 player
= 'mplayer -fs %F'
2209 elif file_type
== 'audio' and self
.config
.player
and \
2210 self
.config
.player
!= 'default':
2211 player
= self
.config
.player
2215 if file_type
not in ('audio', 'video') or \
2216 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2217 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2218 # Mark episode as played in the database
2219 episode
.mark(is_played
=True)
2220 self
.mygpo_client
.on_playback([episode
])
2222 filename
= episode
.local_filename(create
=False)
2223 if filename
is None or not os
.path
.exists(filename
):
2224 filename
= episode
.url
2225 if youtube
.is_video_link(filename
):
2226 fmt_id
= self
.config
.youtube_preferred_fmt_id
2227 if gpodder
.ui
.fremantle
:
2229 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2231 # Determine the playback resume position - if the file
2232 # was played 100%, we simply start from the beginning
2233 resume_position
= episode
.current_position
2234 if resume_position
== episode
.total_time
:
2237 if gpodder
.ui
.fremantle
:
2238 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2240 # If Panucci is configured, use D-Bus on Maemo to call it
2241 if player
== 'panucci':
2243 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2244 PANUCCI_PATH
= '/panucciInterface'
2245 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2246 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2247 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2249 def on_reply(*args
):
2252 def error_handler(filename
, err
):
2253 log('Exception in D-Bus call: %s', str(err
), \
2256 # Fallback: use the command line client
2257 for command
in util
.format_desktop_command('panucci', \
2259 log('Executing: %s', repr(command
), sender
=self
)
2260 subprocess
.Popen(command
)
2262 on_error
= lambda err
: error_handler(filename
, err
)
2264 # This method only exists in Panucci > 0.9 ('new Panucci')
2265 i
.playback_from(filename
, resume_position
, \
2266 reply_handler
=on_reply
, error_handler
=on_error
)
2268 continue # This file was handled by the D-Bus call
2269 except Exception, e
:
2270 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2271 elif player
== 'MediaBox' and gpodder
.ui
.maemo
:
2273 MEDIABOX_NAME
= 'de.pycage.mediabox'
2274 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2275 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2276 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2277 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2279 def on_reply(*args
):
2283 log('Exception in D-Bus call: %s', str(err
), \
2286 i
.load(filename
, '%s/x-unknown' % file_type
, \
2287 reply_handler
=on_reply
, error_handler
=on_error
)
2289 continue # This file was handled by the D-Bus call
2290 except Exception, e
:
2291 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2293 groups
[player
].append(filename
)
2295 # Open episodes with system default player
2296 if 'default' in groups
:
2297 if gpodder
.ui
.maemo
and len(groups
['default']) > 1:
2298 # The Nokia Media Player app does not support receiving multiple
2299 # file names via D-Bus, so we simply place all file names into a
2300 # temporary M3U playlist and open that with the Media Player.
2301 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2302 util
.write_m3u_playlist(m3u_filename
, groups
['default'], extm3u
=False)
2303 util
.gui_open(m3u_filename
)
2305 for filename
in groups
['default']:
2306 log('Opening with system default: %s', filename
, sender
=self
)
2307 util
.gui_open(filename
)
2308 del groups
['default']
2309 elif gpodder
.ui
.maemo
and groups
:
2310 # When on Maemo and not opening with default, show a notification
2311 # (no startup notification for Panucci / MPlayer yet...)
2312 if len(episodes
) == 1:
2313 text
= _('Opening %s') % episodes
[0].title
2315 count
= len(episodes
)
2316 text
= N_('Opening %(count)d episode', 'Opening %(count)d episodes', count
) % {'count':count
}
2318 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2320 def destroy_banner_later(banner
):
2323 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2325 # For each type now, go and create play commands
2326 for group
in groups
:
2327 for command
in util
.format_desktop_command(group
, groups
[group
]):
2328 log('Executing: %s', repr(command
), sender
=self
)
2329 subprocess
.Popen(command
)
2331 # Persist episode status changes to the database
2334 # Flush updated episode status
2335 self
.mygpo_client
.flush()
2337 def playback_episodes(self
, episodes
):
2338 # We need to create a list, because we run through it more than once
2339 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2340 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2343 self
.playback_episodes_for_real(episodes
)
2344 except Exception, e
:
2345 log('Error in playback!', sender
=self
, traceback
=True)
2346 if gpodder
.ui
.desktop
:
2347 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2348 _('Error opening player'), widget
=self
.toolPreferences
)
2350 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2352 channel_urls
= set()
2353 episode_urls
= set()
2354 for episode
in episodes
:
2355 channel_urls
.add(episode
.channel
.url
)
2356 episode_urls
.add(episode
.url
)
2357 self
.update_episode_list_icons(episode_urls
)
2358 self
.update_podcast_list_model(channel_urls
)
2360 def play_or_download(self
):
2361 if not gpodder
.ui
.fremantle
:
2362 if self
.wNotebook
.get_current_page() > 0:
2363 if gpodder
.ui
.desktop
:
2364 self
.toolCancel
.set_sensitive(True)
2367 if self
.currently_updating
:
2368 return (False, False, False, False, False, False)
2370 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2371 ( is_played
, is_locked
) = (False,)*2
2373 open_instead_of_play
= False
2375 selection
= self
.treeAvailable
.get_selection()
2376 if selection
.count_selected_rows() > 0:
2377 (model
, paths
) = selection
.get_selected_rows()
2381 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2382 except TypeError, te
:
2383 log('Invalid episode at path %s', str(path
), sender
=self
)
2386 if episode
.file_type() not in ('audio', 'video'):
2387 open_instead_of_play
= True
2389 if episode
.was_downloaded():
2390 can_play
= episode
.was_downloaded(and_exists
=True)
2391 is_played
= episode
.is_played
2392 is_locked
= episode
.is_locked
2396 if self
.episode_is_downloading(episode
):
2401 can_download
= can_download
and not can_cancel
2402 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2403 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2404 can_delete
= not can_cancel
2406 if gpodder
.ui
.desktop
:
2407 if open_instead_of_play
:
2408 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2410 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2411 self
.toolPlay
.set_sensitive( can_play
)
2412 self
.toolDownload
.set_sensitive( can_download
)
2413 self
.toolTransfer
.set_sensitive( can_transfer
)
2414 self
.toolCancel
.set_sensitive( can_cancel
)
2416 if not gpodder
.ui
.fremantle
:
2417 self
.item_cancel_download
.set_sensitive(can_cancel
)
2418 self
.itemDownloadSelected
.set_sensitive(can_download
)
2419 self
.itemOpenSelected
.set_sensitive(can_play
)
2420 self
.itemPlaySelected
.set_sensitive(can_play
)
2421 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2422 self
.item_toggle_played
.set_sensitive(can_play
)
2423 self
.item_toggle_lock
.set_sensitive(can_play
)
2424 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2425 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2427 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2429 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2430 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2432 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2433 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2435 def episode_new_status_changed(self
, urls
):
2436 self
.update_podcast_list_model()
2437 self
.update_episode_list_icons(urls
)
2439 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2440 """Update the podcast list treeview model
2442 If urls is given, it should list the URLs of each
2443 podcast that has to be updated in the list.
2445 If selected is True, only update the model contents
2446 for the currently-selected podcast - nothing more.
2448 The caller can optionally specify "select_url",
2449 which is the URL of the podcast that is to be
2450 selected in the list after the update is complete.
2451 This only works if the podcast list has to be
2452 reloaded; i.e. something has been added or removed
2453 since the last update of the podcast list).
2455 selection
= self
.treeChannels
.get_selection()
2456 model
, iter = selection
.get_selected()
2458 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2459 # Update "all episodes" view in any case (if enabled)
2460 self
.podcast_list_model
.update_first_row()
2463 # very cheap! only update selected channel
2464 if iter is not None:
2465 # If we have selected the "all episodes" view, we have
2466 # to update all channels for selected episodes:
2467 if self
.config
.podcast_list_view_all
and \
2468 self
.podcast_list_model
.iter_is_first_row(iter):
2469 urls
= self
.get_podcast_urls_from_selected_episodes()
2470 self
.podcast_list_model
.update_by_urls(urls
)
2472 # Otherwise just update the selected row (a podcast)
2473 self
.podcast_list_model
.update_by_filter_iter(iter)
2474 elif not self
.channel_list_changed
:
2475 # we can keep the model, but have to update some
2477 # still cheaper than reloading the whole list
2478 self
.podcast_list_model
.update_all()
2480 # ok, we got a bunch of urls to update
2481 self
.podcast_list_model
.update_by_urls(urls
)
2483 if model
and iter and select_url
is None:
2484 # Get the URL of the currently-selected podcast
2485 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2487 # Update the podcast list model with new channels
2488 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2491 selected_iter
= model
.get_iter_first()
2492 # Find the previously-selected URL in the new
2493 # model if we have an URL (else select first)
2494 if select_url
is not None:
2495 pos
= model
.get_iter_first()
2496 while pos
is not None:
2497 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2498 if url
== select_url
:
2501 pos
= model
.iter_next(pos
)
2503 if not gpodder
.ui
.maemo
:
2504 if selected_iter
is not None:
2505 selection
.select_iter(selected_iter
)
2506 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2508 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2509 self
.channel_list_changed
= False
2511 def episode_is_downloading(self
, episode
):
2512 """Returns True if the given episode is being downloaded at the moment"""
2516 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2518 def on_episode_list_filter_changed(self
, has_episodes
):
2519 if gpodder
.ui
.fremantle
:
2521 self
.episodes_window
.empty_label
.hide()
2522 self
.episodes_window
.pannablearea
.show()
2524 if self
.config
.episode_list_view_mode
!= \
2525 EpisodeListModel
.VIEW_ALL
:
2526 text
= _('No episodes in current view')
2528 text
= _('No episodes available')
2529 self
.episodes_window
.empty_label
.set_text(text
)
2530 self
.episodes_window
.pannablearea
.hide()
2531 self
.episodes_window
.empty_label
.show()
2533 def update_episode_list_model(self
):
2534 if self
.channels
and self
.active_channel
is not None:
2535 if gpodder
.ui
.fremantle
:
2536 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2538 self
.currently_updating
= True
2539 self
.episode_list_model
.clear()
2540 if gpodder
.ui
.fremantle
:
2541 self
.episodes_window
.pannablearea
.hide()
2542 self
.episodes_window
.empty_label
.set_text(_('Loading episodes'))
2543 self
.episodes_window
.empty_label
.show()
2546 additional_args
= (self
.episode_is_downloading
, \
2547 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2548 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2549 self
.episode_list_model
.replace_from_channel(self
.active_channel
, *additional_args
)
2551 self
.treeAvailable
.get_selection().unselect_all()
2552 self
.treeAvailable
.scroll_to_point(0, 0)
2554 self
.currently_updating
= False
2555 self
.play_or_download()
2557 if gpodder
.ui
.fremantle
:
2558 hildon
.hildon_gtk_window_set_progress_indicator(\
2559 self
.episodes_window
.main_window
, False)
2561 util
.idle_add(update
)
2563 self
.episode_list_model
.clear()
2565 @dbus.service
.method(gpodder
.dbus_interface
)
2566 def offer_new_episodes(self
, channels
=None):
2567 if gpodder
.ui
.fremantle
:
2568 # Assume that when this function is called that the
2569 # notification is not shown anymore (Maemo bug 11345)
2570 self
._fremantle
_notification
_visible
= False
2572 new_episodes
= self
.get_new_episodes(channels
)
2574 self
.new_episodes_show(new_episodes
)
2578 def add_podcast_list(self
, urls
, auth_tokens
=None):
2579 """Subscribe to a list of podcast given their URLs
2581 If auth_tokens is given, it should be a dictionary
2582 mapping URLs to (username, password) tuples."""
2584 if auth_tokens
is None:
2587 # Sort and split the URL list into five buckets
2588 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2589 for input_url
in urls
:
2590 url
= util
.normalize_feed_url(input_url
)
2592 # Fail this one because the URL is not valid
2593 failed
.append(input_url
)
2594 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2595 # A podcast already exists in the list for this URL
2596 existing
.append(url
)
2598 # This URL has survived the first round - queue for add
2600 if url
!= input_url
and input_url
in auth_tokens
:
2601 auth_tokens
[url
] = auth_tokens
[input_url
]
2606 progress
= ProgressIndicator(_('Adding podcasts'), \
2607 _('Please wait while episode information is downloaded.'), \
2608 parent
=self
.get_dialog_parent())
2610 def on_after_update():
2611 progress
.on_finished()
2612 # Report already-existing subscriptions to the user
2614 title
= _('Existing subscriptions skipped')
2615 message
= _('You are already subscribed to these podcasts:') \
2616 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2617 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2619 # Report subscriptions that require authentication
2623 title
= _('Podcast requires authentication')
2624 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2625 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2627 retry_podcasts
[url
] = auth_tokens
2629 # Stop asking the user for more login data
2632 error_messages
[url
] = _('Authentication failed')
2636 # If we have authentication data to retry, do so here
2638 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2640 # Report website redirections
2641 for url
in redirections
:
2642 title
= _('Website redirection detected')
2643 message
= _('The URL %(url)s redirects to %(target)s.') \
2644 + '\n\n' + _('Do you want to visit the website now?')
2645 message
= message
% {'url': url
, 'target': redirections
[url
]}
2646 if self
.show_confirmation(message
, title
):
2647 util
.open_website(url
)
2651 # Report failed subscriptions to the user
2653 title
= _('Could not add some podcasts')
2654 message
= _('Some podcasts could not be added to your list:') \
2655 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2656 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2657 self
.show_message(message
, title
, important
=True)
2659 # Upload subscription changes to gpodder.net
2660 self
.mygpo_client
.on_subscribe(worked
)
2662 # If at least one podcast has been added, save and update all
2663 if self
.channel_list_changed
:
2664 # Fix URLs if mygpo has rewritten them
2665 self
.rewrite_urls_mygpo()
2667 self
.save_channels_opml()
2669 # If only one podcast was added, select it after the update
2670 if len(worked
) == 1:
2675 # Update the list of subscribed podcasts
2676 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2677 self
.update_podcasts_tab()
2679 # Offer to download new episodes
2681 for podcast
in self
.channels
:
2682 if podcast
.url
in worked
:
2683 episodes
.extend(podcast
.get_all_episodes())
2686 episodes
= list(PodcastEpisode
.sort_by_pubdate(episodes
, \
2688 self
.new_episodes_show(episodes
, \
2689 selected
=[e
.check_is_new() for e
in episodes
])
2693 # After the initial sorting and splitting, try all queued podcasts
2694 length
= len(queued
)
2695 for index
, url
in enumerate(queued
):
2696 progress
.on_progress(float(index
)/float(length
))
2697 progress
.on_message(url
)
2698 log('QUEUE RUNNER: %s', url
, sender
=self
)
2700 # The URL is valid and does not exist already - subscribe!
2701 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2702 authentication_tokens
=auth_tokens
.get(url
, None), \
2703 max_episodes
=self
.config
.max_episodes_per_feed
, \
2704 download_dir
=self
.config
.download_dir
, \
2705 allow_empty_feeds
=self
.config
.allow_empty_feeds
, \
2706 mimetype_prefs
=self
.config
.mimetype_prefs
)
2709 username
, password
= util
.username_password_from_url(url
)
2710 except ValueError, ve
:
2711 username
, password
= (None, None)
2713 if username
is not None and channel
.username
is None and \
2714 password
is not None and channel
.password
is None:
2715 channel
.username
= username
2716 channel
.password
= password
2719 self
._update
_cover
(channel
)
2720 except feedcore
.AuthenticationRequired
:
2721 if url
in auth_tokens
:
2722 # Fail for wrong authentication data
2723 error_messages
[url
] = _('Authentication failed')
2726 # Queue for login dialog later
2729 except feedcore
.WifiLogin
, error
:
2730 redirections
[url
] = error
.data
2732 error_messages
[url
] = _('Redirection detected')
2734 except Exception, e
:
2735 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2736 error_messages
[url
] = str(e
)
2740 assert channel
is not None
2741 worked
.append(channel
.url
)
2742 self
.channels
.append(channel
)
2743 self
.channel_list_changed
= True
2744 util
.idle_add(on_after_update
)
2745 threading
.Thread(target
=thread_proc
).start()
2747 def save_channels_opml(self
):
2748 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2749 return exporter
.write(self
.channels
)
2751 def find_episode(self
, podcast_url
, episode_url
):
2752 """Find an episode given its podcast and episode URL
2754 The function will return a PodcastEpisode object if
2755 the episode is found, or None if it's not found.
2757 for podcast
in self
.channels
:
2758 if podcast_url
== podcast
.url
:
2759 for episode
in podcast
.get_all_episodes():
2760 if episode_url
== episode
.url
:
2765 def process_received_episode_actions(self
, updated_urls
):
2766 """Process/merge episode actions from gpodder.net
2768 This function will merge all changes received from
2769 the server to the local database and update the
2770 status of the affected episodes as necessary.
2772 indicator
= ProgressIndicator(_('Merging episode actions'), \
2773 _('Episode actions from gpodder.net are merged.'), \
2774 False, self
.get_dialog_parent())
2776 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2777 if action
.action
== 'play':
2778 episode
= self
.find_episode(action
.podcast_url
, \
2781 if episode
is not None:
2782 log('Play action for %s', episode
.url
, sender
=self
)
2783 episode
.mark(is_played
=True)
2785 if action
.timestamp
> episode
.current_position_updated
and \
2786 action
.position
is not None:
2787 log('Updating position for %s', episode
.url
, sender
=self
)
2788 episode
.current_position
= action
.position
2789 episode
.current_position_updated
= action
.timestamp
2792 log('Updating total time for %s', episode
.url
, sender
=self
)
2793 episode
.total_time
= action
.total
2796 elif action
.action
== 'delete':
2797 episode
= self
.find_episode(action
.podcast_url
, \
2800 if episode
is not None:
2801 if not episode
.was_downloaded(and_exists
=True):
2802 # Set the episode to a "deleted" state
2803 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2804 episode
.delete_from_disk()
2807 indicator
.on_message(N_('%(count)d action processed', '%(count)d actions processed', idx
) % {'count':idx
})
2808 gtk
.main_iteration(False)
2810 indicator
.on_finished()
2814 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2816 self
.updating_feed_cache
= False
2818 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2820 # Process received episode actions for all updated URLs
2821 self
.process_received_episode_actions(updated_urls
)
2823 self
.channel_list_changed
= True
2824 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2826 # Only search for new episodes in podcasts that have been
2827 # updated, not in other podcasts (for single-feed updates)
2828 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2830 if gpodder
.ui
.fremantle
:
2831 self
.fancy_progress_bar
.hide()
2832 self
.button_subscribe
.set_sensitive(True)
2833 self
.button_refresh
.set_sensitive(True)
2834 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2835 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2836 self
.update_podcasts_tab()
2837 self
.update_episode_list_model()
2838 if self
.feed_cache_update_cancelled
:
2841 def application_in_foreground():
2843 return any(w
.get_property('is-topmost') for w
in hildon
.WindowStack
.get_default().get_windows())
2844 except Exception, e
:
2845 log('Could not determine is-topmost', traceback
=True)
2846 # When in doubt, assume not in foreground
2850 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2851 # New episodes found, but we should do nothing
2852 self
.show_message(_('New episodes are available.'))
2853 elif self
.config
.auto_download
== 'always':
2854 count
= len(episodes
)
2855 title
= N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count
) % {'count':count
}
2856 self
.show_message(title
)
2857 self
.download_episode_list(episodes
)
2858 elif self
.config
.auto_download
== 'queue':
2859 self
.show_message(_('New episodes have been added to the download list.'))
2860 self
.download_episode_list_paused(episodes
)
2861 elif application_in_foreground():
2862 if not self
._fremantle
_notification
_visible
:
2863 self
.new_episodes_show(episodes
)
2864 elif not self
._fremantle
_notification
_visible
:
2867 pynotify
.init('gPodder')
2868 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2869 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2870 n
.set_hint('dbus-callback-default', ' '.join([
2871 gpodder
.dbus_bus_name
,
2872 gpodder
.dbus_gui_object_path
,
2873 gpodder
.dbus_interface
,
2874 'offer_new_episodes',
2876 n
.set_category('gpodder-new-episodes')
2878 self
._fremantle
_notification
_visible
= True
2879 except Exception, e
:
2880 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2881 self
.new_episodes_show(episodes
)
2882 self
._fremantle
_notification
_visible
= False
2883 elif not self
.config
.auto_update_feeds
:
2884 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2888 self
.tray_icon
.set_status()
2890 if self
.feed_cache_update_cancelled
:
2891 # The user decided to abort the feed update
2892 self
.show_update_feeds_buttons()
2894 # Nothing new here - but inform the user
2895 self
.pbFeedUpdate
.set_fraction(1.0)
2896 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2897 self
.feed_cache_update_cancelled
= True
2898 self
.btnCancelFeedUpdate
.show()
2899 self
.btnCancelFeedUpdate
.set_sensitive(True)
2900 self
.itemUpdate
.set_sensitive(True)
2901 if gpodder
.ui
.maemo
:
2902 # btnCancelFeedUpdate is a ToolButton on Maemo
2903 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2905 # btnCancelFeedUpdate is a normal gtk.Button
2906 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2908 count
= len(episodes
)
2909 # New episodes are available
2910 self
.pbFeedUpdate
.set_fraction(1.0)
2911 # Are we minimized and should we auto download?
2912 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2913 self
.download_episode_list(episodes
)
2914 title
= N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count
) % {'count':count
}
2915 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2916 self
.show_update_feeds_buttons()
2917 elif self
.config
.auto_download
== 'queue':
2918 self
.download_episode_list_paused(episodes
)
2919 title
= N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count
) % {'count':count
}
2920 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2921 self
.show_update_feeds_buttons()
2923 self
.show_update_feeds_buttons()
2924 # New episodes are available and we are not minimized
2925 if not self
.config
.do_not_show_new_episodes_dialog
:
2926 self
.new_episodes_show(episodes
, notification
=True)
2928 message
= N_('%(count)d new episode available', '%(count)d new episodes available', count
) % {'count':count
}
2929 self
.pbFeedUpdate
.set_text(message
)
2931 def _update_cover(self
, channel
):
2932 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2933 self
.cover_downloader
.request_cover(channel
)
2935 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2936 total
= len(channels
)
2938 for updated
, channel
in enumerate(channels
):
2939 if not self
.feed_cache_update_cancelled
:
2941 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
, \
2942 mimetype_prefs
=self
.config
.mimetype_prefs
)
2943 self
._update
_cover
(channel
)
2944 except Exception, e
:
2945 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2947 message
= _('Error while updating %(url)s: %(message)s')
2949 message
= _('The feed at %(url)s could not be updated.')
2950 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2951 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2953 if self
.feed_cache_update_cancelled
:
2956 # By the time we get here the update may have already been cancelled
2957 if not self
.feed_cache_update_cancelled
:
2958 def update_progress():
2959 d
= {'podcast': channel
.title
, 'position': updated
+1, 'total': total
}
2960 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2961 self
.pbFeedUpdate
.set_text(progression
)
2963 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2964 self
.pbFeedUpdate
.set_fraction(float(updated
+1)/float(total
))
2965 util
.idle_add(update_progress
)
2967 updated_urls
= [c
.url
for c
in channels
]
2968 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2970 def show_update_feeds_buttons(self
):
2971 # Make sure that the buttons for updating feeds
2972 # appear - this should happen after a feed update
2973 if gpodder
.ui
.maemo
:
2974 self
.btnUpdateSelectedFeed
.show()
2975 self
.toolFeedUpdateProgress
.hide()
2976 self
.btnCancelFeedUpdate
.hide()
2977 self
.btnCancelFeedUpdate
.set_is_important(False)
2978 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2979 self
.toolbarSpacer
.set_expand(True)
2980 self
.toolbarSpacer
.set_draw(False)
2982 self
.hboxUpdateFeeds
.hide()
2983 self
.btnUpdateFeeds
.show()
2984 self
.itemUpdate
.set_sensitive(True)
2985 self
.itemUpdateChannel
.set_sensitive(True)
2987 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2988 if not self
.feed_cache_update_cancelled
:
2989 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2990 self
.feed_cache_update_cancelled
= True
2991 if not gpodder
.ui
.fremantle
:
2992 self
.btnCancelFeedUpdate
.set_sensitive(False)
2993 elif not gpodder
.ui
.fremantle
:
2994 self
.show_update_feeds_buttons()
2996 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2997 if self
.updating_feed_cache
:
2998 if gpodder
.ui
.fremantle
:
2999 self
.feed_cache_update_cancelled
= True
3002 if not force_update
:
3003 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
3004 self
.channel_list_changed
= True
3005 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
3008 # Fix URLs if mygpo has rewritten them
3009 self
.rewrite_urls_mygpo()
3011 self
.updating_feed_cache
= True
3013 if channels
is None:
3014 # Only update podcasts for which updates are enabled
3015 channels
= [c
for c
in self
.channels
if c
.feed_update_enabled
]
3017 if gpodder
.ui
.fremantle
:
3018 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
3019 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
3020 self
.fancy_progress_bar
.show()
3021 self
.button_subscribe
.set_sensitive(False)
3022 self
.button_refresh
.set_sensitive(False)
3023 self
.feed_cache_update_cancelled
= False
3025 self
.itemUpdate
.set_sensitive(False)
3026 self
.itemUpdateChannel
.set_sensitive(False)
3029 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
3031 self
.feed_cache_update_cancelled
= False
3032 self
.btnCancelFeedUpdate
.show()
3033 self
.btnCancelFeedUpdate
.set_sensitive(True)
3034 if gpodder
.ui
.maemo
:
3035 self
.toolbarSpacer
.set_expand(False)
3036 self
.toolbarSpacer
.set_draw(True)
3037 self
.btnUpdateSelectedFeed
.hide()
3038 self
.toolFeedUpdateProgress
.show_all()
3040 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
3041 self
.hboxUpdateFeeds
.show_all()
3042 self
.btnUpdateFeeds
.hide()
3044 if len(channels
) == 1:
3045 text
= _('Updating "%s"...') % channels
[0].title
3047 count
= len(channels
)
3048 text
= N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count
) % {'count':count
}
3049 self
.pbFeedUpdate
.set_text(text
)
3050 self
.pbFeedUpdate
.set_fraction(0)
3052 args
= (channels
, select_url_afterwards
)
3053 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
3055 def on_gPodder_delete_event(self
, widget
, *args
):
3056 """Called when the GUI wants to close the window
3057 Displays a confirmation dialog (and closes/hides gPodder)
3060 downloading
= self
.download_status_model
.are_downloads_in_progress()
3062 # Only iconify if we are using the window's "X" button,
3063 # but not when we are using "Quit" in the menu or toolbar
3064 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
3065 self
.iconify_main_window()
3067 if gpodder
.ui
.fremantle
:
3068 self
.close_gpodder()
3069 elif gpodder
.ui
.diablo
:
3070 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
3072 self
.close_gpodder()
3075 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
3076 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3077 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
3079 title
= _('Quit gPodder')
3080 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3082 dialog
.set_title(title
)
3083 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
3085 quit_button
.grab_focus()
3086 result
= dialog
.run()
3089 if result
== gtk
.RESPONSE_CLOSE
:
3090 self
.close_gpodder()
3092 self
.close_gpodder()
3096 def close_gpodder(self
):
3097 """ clean everything and exit properly
3100 if self
.save_channels_opml():
3101 pass # FIXME: Add mygpo synchronization here
3103 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
3107 if self
.tray_icon
is not None:
3108 self
.tray_icon
.set_visible(False)
3110 # Notify all tasks to to carry out any clean-up actions
3111 self
.download_status_model
.tell_all_tasks_to_quit()
3113 while gtk
.events_pending():
3114 gtk
.main_iteration(False)
3121 def get_expired_episodes(self
):
3122 for channel
in self
.channels
:
3123 for episode
in channel
.get_downloaded_episodes():
3124 # Never consider locked episodes as old
3125 if episode
.is_locked
:
3128 # Never consider fresh episodes as old
3129 if episode
.age_in_days() < self
.config
.episode_old_age
:
3132 # Do not delete played episodes (except if configured)
3133 if episode
.is_played
:
3134 if not self
.config
.auto_remove_played_episodes
:
3137 # Do not delete unplayed episodes (except if configured)
3138 if not episode
.is_played
:
3139 if not self
.config
.auto_remove_unplayed_episodes
:
3144 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3149 episodes
= [e
for e
in episodes
if not e
.is_locked
]
3152 title
= _('Episodes are locked')
3153 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3154 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3157 count
= len(episodes
)
3158 title
= N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count
) % {'count':count
}
3159 message
= _('Deleting episodes removes downloaded files.')
3161 if gpodder
.ui
.fremantle
:
3162 message
= '\n'.join([title
, message
])
3164 if confirm
and not self
.show_confirmation(message
, title
):
3167 progress
= ProgressIndicator(_('Deleting episodes'), \
3168 _('Please wait while episodes are deleted'), \
3169 parent
=self
.get_dialog_parent())
3171 def finish_deletion(episode_urls
, channel_urls
):
3172 progress
.on_finished()
3174 # Episodes have been deleted - persist the database
3177 self
.update_episode_list_icons(episode_urls
)
3178 self
.update_podcast_list_model(channel_urls
)
3179 self
.play_or_download()
3182 episode_urls
= set()
3183 channel_urls
= set()
3185 episodes_status_update
= []
3186 for idx
, episode
in enumerate(episodes
):
3187 progress
.on_progress(float(idx
)/float(len(episodes
)))
3188 if episode
.is_locked
and skip_locked
:
3189 log('Not deleting episode (is locked): %s', episode
.title
)
3191 log('Deleting episode: %s', episode
.title
)
3192 progress
.on_message(episode
.title
)
3193 episode
.delete_from_disk()
3194 episode_urls
.add(episode
.url
)
3195 channel_urls
.add(episode
.channel
.url
)
3196 episodes_status_update
.append(episode
)
3198 # Tell the shownotes window that we have removed the episode
3199 if self
.episode_shownotes_window
is not None and \
3200 self
.episode_shownotes_window
.episode
is not None and \
3201 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3202 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3204 # Notify the web service about the status update + upload
3205 self
.mygpo_client
.on_delete(episodes_status_update
)
3206 self
.mygpo_client
.flush()
3208 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3210 threading
.Thread(target
=thread_proc
).start()
3214 def on_itemRemoveOldEpisodes_activate(self
, widget
):
3215 self
.show_delete_episodes_window()
3217 def show_delete_episodes_window(self
, channel
=None):
3218 """Offer deletion of episodes
3220 If channel is None, offer deletion of all episodes.
3221 Otherwise only offer deletion of episodes in the channel.
3223 if gpodder
.ui
.maemo
:
3225 ('maemo_remove_markup', None, None, _('Episode')),
3229 ('title_markup', None, None, _('Episode')),
3230 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3231 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3232 ('played_prop', None, None, _('Status')),
3233 ('age_prop', 'age_int_prop', gobject
.TYPE_INT
, _('Downloaded')),
3236 msg_older_than
= N_('Select older than %(count)d day', 'Select older than %(count)d days', self
.config
.episode_old_age
)
3237 selection_buttons
= {
3238 _('Select played'): lambda episode
: episode
.is_played
,
3239 _('Select finished'): lambda episode
: episode
.is_finished(),
3240 msg_older_than
% {'count':self
.config
.episode_old_age
}: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3243 instructions
= _('Select the episodes you want to delete:')
3246 channels
= self
.channels
3248 channels
= [channel
]
3251 for channel
in channels
:
3252 for episode
in channel
.get_downloaded_episodes():
3253 # Disallow deletion of locked episodes that still exist
3254 if not episode
.is_locked
or not episode
.file_exists():
3255 episodes
.append(episode
)
3257 selected
= [e
for e
in episodes
if episode
.is_played
or not episode
.file_exists()]
3259 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3260 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3261 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3262 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3263 show_episode_shownotes
=self
.show_episode_shownotes
)
3265 def on_selected_episodes_status_changed(self
):
3266 # The order of the updates here is important! When "All episodes" is
3267 # selected, the update of the podcast list model depends on the episode
3268 # list selection to determine which podcasts are affected. Updating
3269 # the episode list could remove the selection if a filter is active.
3270 self
.update_podcast_list_model(selected
=True)
3271 self
.update_episode_list_icons(selected
=True)
3274 def mark_selected_episodes_new(self
):
3275 for episode
in self
.get_selected_episodes():
3277 self
.on_selected_episodes_status_changed()
3279 def mark_selected_episodes_old(self
):
3280 for episode
in self
.get_selected_episodes():
3282 self
.on_selected_episodes_status_changed()
3284 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3285 for episode
in self
.get_selected_episodes():
3287 episode
.mark(is_played
=not episode
.is_played
)
3289 episode
.mark(is_played
=new_value
)
3290 self
.on_selected_episodes_status_changed()
3292 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3293 for episode
in self
.get_selected_episodes():
3295 episode
.mark(is_locked
=not episode
.is_locked
)
3297 episode
.mark(is_locked
=new_value
)
3298 self
.on_selected_episodes_status_changed()
3300 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3301 if self
.active_channel
is None:
3304 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3305 self
.active_channel
.update_channel_lock()
3307 for episode
in self
.active_channel
.get_all_episodes():
3308 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3310 self
.update_podcast_list_model(selected
=True)
3311 self
.update_episode_list_icons(all
=True)
3313 def on_itemUpdateChannel_activate(self
, widget
=None):
3314 if self
.active_channel
is None:
3315 title
= _('No podcast selected')
3316 message
= _('Please select a podcast in the podcasts list to update.')
3317 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3320 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3321 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3322 self
.update_feed_cache()
3324 self
.update_feed_cache(channels
=[self
.active_channel
])
3326 def on_itemUpdate_activate(self
, widget
=None):
3327 # Check if we have outstanding subscribe/unsubscribe actions
3328 if self
.on_add_remove_podcasts_mygpo():
3329 log('Update cancelled (received server changes)', sender
=self
)
3333 self
.update_feed_cache()
3335 gPodderWelcome(self
.gPodder
,
3336 center_on_widget
=self
.gPodder
,
3337 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3338 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3340 def download_episode_list_paused(self
, episodes
):
3341 self
.download_episode_list(episodes
, True)
3343 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3344 enable_update
= False
3346 for episode
in episodes
:
3347 log('Downloading episode: %s', episode
.title
, sender
= self
)
3348 if not episode
.was_downloaded(and_exists
=True):
3350 for task
in self
.download_tasks_seen
:
3351 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3352 self
.download_queue_manager
.add_task(task
, force_start
)
3353 enable_update
= True
3361 task
= download
.DownloadTask(episode
, self
.config
)
3362 except Exception, e
:
3363 d
= {'episode': episode
.title
, 'message': str(e
)}
3364 message
= _('Download error while downloading %(episode)s: %(message)s')
3365 self
.show_message(message
% d
, _('Download error'), important
=True)
3366 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3370 task
.status
= task
.PAUSED
3372 self
.mygpo_client
.on_download([task
.episode
])
3373 self
.download_queue_manager
.add_task(task
, force_start
)
3375 self
.download_status_model
.register_task(task
)
3376 enable_update
= True
3379 self
.enable_download_list_update()
3381 # Flush updated episode status
3382 self
.mygpo_client
.flush()
3384 def cancel_task_list(self
, tasks
):
3389 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3390 task
.status
= task
.CANCELLED
3391 elif task
.status
== task
.PAUSED
:
3392 task
.status
= task
.CANCELLED
3393 # Call run, so the partial file gets deleted
3396 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3397 self
.play_or_download()
3399 # Update the tab title and downloads list
3400 self
.update_downloads_list()
3402 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3403 if gpodder
.ui
.maemo
:
3405 ('maemo_markup', None, None, _('Episode')),
3407 show_notification
= notification
3410 ('title_markup', None, None, _('Episode')),
3411 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3412 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3414 show_notification
= False
3416 instructions
= _('Select the episodes you want to download:')
3418 if self
.new_episodes_window
is not None:
3419 self
.new_episodes_window
.main_window
.destroy()
3420 self
.new_episodes_window
= None
3422 def download_episodes_callback(episodes
):
3423 self
.new_episodes_window
= None
3424 self
.download_episode_list(episodes
)
3426 if selected
is None:
3427 # Select all by default
3428 selected
= [True]*len(episodes
)
3430 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3431 title
=_('New episodes available'), \
3432 instructions
=instructions
, \
3433 episodes
=episodes
, \
3435 selected
=selected
, \
3436 stock_ok_button
= 'gpodder-download', \
3437 callback
=download_episodes_callback
, \
3438 remove_callback
=lambda e
: e
.mark_old(), \
3439 remove_action
=_('Mark as old'), \
3440 remove_finished
=self
.episode_new_status_changed
, \
3441 _config
=self
.config
, \
3442 show_notification
=show_notification
, \
3443 show_episode_shownotes
=self
.show_episode_shownotes
)
3445 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3446 if not self
.offer_new_episodes():
3447 self
.show_message(_('Please check for new episodes later.'), \
3448 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3450 def get_new_episodes(self
, channels
=None):
3451 if channels
is None:
3452 channels
= self
.channels
3454 for channel
in channels
:
3455 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3456 episodes
.append(episode
)
3460 @dbus.service
.method(gpodder
.dbus_interface
)
3461 def start_device_synchronization(self
):
3462 """Public D-Bus API for starting Device sync (Desktop only)
3464 This method can be called to initiate a synchronization with
3465 a configured protable media player. This only works for the
3466 Desktop version of gPodder and does nothing on Maemo.
3468 if gpodder
.ui
.desktop
:
3469 self
.on_sync_to_ipod_activate(None)
3474 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3475 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3477 def commit_changes_to_database(self
):
3478 """This will be called after the sync process is finished"""
3481 def on_cleanup_ipod_activate(self
, widget
, *args
):
3482 self
.sync_ui
.on_cleanup_device()
3484 def on_manage_device_playlist(self
, widget
):
3485 self
.sync_ui
.on_manage_device_playlist()
3487 def show_hide_tray_icon(self
):
3488 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3489 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3490 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3491 self
.tray_icon
.set_visible(False)
3493 self
.tray_icon
= None
3495 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3496 self
.tray_icon
.set_visible(self
.is_iconified())
3497 elif self
.tray_icon
:
3498 self
.tray_icon
.set_visible(True)
3500 def on_itemShowAllEpisodes_activate(self
, widget
):
3501 self
.config
.podcast_list_view_all
= widget
.get_active()
3503 def on_itemShowToolbar_activate(self
, widget
):
3504 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3506 def on_itemShowDescription_activate(self
, widget
):
3507 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3509 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3510 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3511 if self
.config
.podcast_list_hide_boring
:
3512 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3514 self
.podcast_list_model
.set_view_mode(-1)
3516 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3518 if current
== self
.item_view_podcasts_all
:
3519 self
.podcast_list_model
.set_view_mode(-1)
3520 elif current
== self
.item_view_podcasts_downloaded
:
3521 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3522 elif current
== self
.item_view_podcasts_unplayed
:
3523 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3525 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3527 def on_item_view_episodes_changed(self
, radioaction
, current
):
3528 if current
== self
.item_view_episodes_all
:
3529 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_ALL
3530 elif current
== self
.item_view_episodes_undeleted
:
3531 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNDELETED
3532 elif current
== self
.item_view_episodes_downloaded
:
3533 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_DOWNLOADED
3534 elif current
== self
.item_view_episodes_unplayed
:
3535 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNPLAYED
3537 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3539 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3540 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3542 def update_item_device( self
):
3543 if not gpodder
.ui
.fremantle
:
3544 if self
.config
.device_type
!= 'none':
3545 self
.itemDevice
.set_visible(True)
3546 self
.itemDevice
.label
= self
.get_device_name()
3548 self
.itemDevice
.set_visible(False)
3550 def properties_closed( self
):
3551 self
.preferences_dialog
= None
3552 self
.show_hide_tray_icon()
3553 self
.update_item_device()
3554 if gpodder
.ui
.maemo
:
3555 selection
= self
.treeAvailable
.get_selection()
3556 if self
.config
.maemo_enable_gestures
or \
3557 self
.config
.enable_fingerscroll
:
3558 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3560 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3562 def on_itemPreferences_activate(self
, widget
, *args
):
3563 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3564 _config
=self
.config
, \
3565 callback_finished
=self
.properties_closed
, \
3566 user_apps_reader
=self
.user_apps_reader
, \
3567 parent_window
=self
.main_window
, \
3568 mygpo_client
=self
.mygpo_client
, \
3569 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3571 # Initial message to relayout window (in case it's opened in portrait mode
3572 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3574 def on_itemDependencies_activate(self
, widget
):
3575 gPodderDependencyManager(self
.gPodder
)
3577 def on_goto_mygpo(self
, widget
):
3578 self
.mygpo_client
.open_website()
3580 def on_download_subscriptions_from_mygpo(self
, action
=None):
3581 title
= _('Login to gpodder.net')
3582 message
= _('Please login to download your subscriptions.')
3583 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3584 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3588 self
.config
.mygpo_username
= username
3589 self
.config
.mygpo_password
= password
3591 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3592 custom_title
=_('Subscriptions on gpodder.net'), \
3593 add_urls_callback
=self
.add_podcast_list
, \
3594 hide_url_entry
=True)
3596 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3597 # we do not have to hardcode the URL here
3598 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3599 url
= util
.url_add_authentication(OPML_URL
, \
3600 self
.config
.mygpo_username
, \
3601 self
.config
.mygpo_password
)
3602 dir.download_opml_file(url
)
3604 def on_mygpo_settings_activate(self
, action
=None):
3605 # This dialog is only used for Maemo 4
3606 if not gpodder
.ui
.diablo
:
3609 settings
= MygPodderSettings(self
.main_window
, \
3610 config
=self
.config
, \
3611 mygpo_client
=self
.mygpo_client
, \
3612 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3614 def on_itemAddChannel_activate(self
, widget
=None):
3615 gPodderAddPodcast(self
.gPodder
, \
3616 add_urls_callback
=self
.add_podcast_list
)
3618 def on_itemEditChannel_activate(self
, widget
, *args
):
3619 if self
.active_channel
is None:
3620 title
= _('No podcast selected')
3621 message
= _('Please select a podcast in the podcasts list to edit.')
3622 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3625 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3626 gPodderChannel(self
.main_window
, \
3627 channel
=self
.active_channel
, \
3628 callback_closed
=callback_closed
, \
3629 cover_downloader
=self
.cover_downloader
)
3631 def on_itemMassUnsubscribe_activate(self
, item
=None):
3633 ('title', None, None, _('Podcast')),
3636 # We're abusing the Episode Selector for selecting Podcasts here,
3637 # but it works and looks good, so why not? -- thp
3638 gPodderEpisodeSelector(self
.main_window
, \
3639 title
=_('Remove podcasts'), \
3640 instructions
=_('Select the podcast you want to remove.'), \
3641 episodes
=self
.channels
, \
3643 size_attribute
=None, \
3644 stock_ok_button
=_('Remove'), \
3645 callback
=self
.remove_podcast_list
, \
3646 _config
=self
.config
)
3648 def remove_podcast_list(self
, channels
, confirm
=True):
3650 log('No podcasts selected for deletion', sender
=self
)
3653 if len(channels
) == 1:
3654 title
= _('Removing podcast')
3655 info
= _('Please wait while the podcast is removed')
3656 message
= _('Do you really want to remove this podcast and its episodes?')
3658 title
= _('Removing podcasts')
3659 info
= _('Please wait while the podcasts are removed')
3660 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3662 if confirm
and not self
.show_confirmation(message
, title
):
3665 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3667 def finish_deletion(select_url
):
3668 # Upload subscription list changes to the web service
3669 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3671 # Re-load the channels and select the desired new channel
3672 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3673 progress
.on_finished()
3674 self
.update_podcasts_tab()
3679 for idx
, channel
in enumerate(channels
):
3680 # Update the UI for correct status messages
3681 progress
.on_progress(float(idx
)/float(len(channels
)))
3682 progress
.on_message(channel
.title
)
3684 # Delete downloaded episodes
3685 channel
.remove_downloaded()
3687 # cancel any active downloads from this channel
3688 for episode
in channel
.get_all_episodes():
3689 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3692 if len(channels
) == 1:
3693 # get the URL of the podcast we want to select next
3694 if channel
in self
.channels
:
3695 position
= self
.channels
.index(channel
)
3699 if position
== len(self
.channels
)-1:
3700 # this is the last podcast, so select the URL
3701 # of the item before this one (i.e. the "new last")
3702 select_url
= self
.channels
[position
-1].url
3704 # there is a podcast after the deleted one, so
3705 # we simply select the one that comes after it
3706 select_url
= self
.channels
[position
+1].url
3708 # Remove the channel and clean the database entries
3710 self
.channels
.remove(channel
)
3712 # Clean up downloads and download directories
3713 self
.clean_up_downloads()
3715 self
.channel_list_changed
= True
3716 self
.save_channels_opml()
3718 # The remaining stuff is to be done in the GTK main thread
3719 util
.idle_add(finish_deletion
, select_url
)
3721 threading
.Thread(target
=thread_proc
).start()
3723 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3724 if self
.active_channel
is None:
3725 title
= _('No podcast selected')
3726 message
= _('Please select a podcast in the podcasts list to remove.')
3727 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3730 self
.remove_podcast_list([self
.active_channel
])
3732 def get_opml_filter(self
):
3733 filter = gtk
.FileFilter()
3734 filter.add_pattern('*.opml')
3735 filter.add_pattern('*.xml')
3736 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3739 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3740 if filename
is None:
3741 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3742 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), \
3743 parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3744 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3745 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3746 elif gpodder
.ui
.diablo
:
3747 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3748 dlg
.set_filter(self
.get_opml_filter())
3749 response
= dlg
.run()
3751 if response
== gtk
.RESPONSE_OK
:
3752 filename
= dlg
.get_filename()
3755 if filename
is not None:
3756 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3757 custom_title
=_('Import podcasts from OPML file'), \
3758 add_urls_callback
=self
.add_podcast_list
, \
3759 hide_url_entry
=True)
3760 dir.download_opml_file(filename
)
3762 def on_itemExportChannels_activate(self
, widget
, *args
):
3763 if not self
.channels
:
3764 title
= _('Nothing to export')
3765 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3766 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3769 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3770 # FIXME: Hildonization on Fremantle
3771 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3772 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3773 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3774 elif gpodder
.ui
.diablo
:
3775 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3776 dlg
.set_filter(self
.get_opml_filter())
3777 response
= dlg
.run()
3778 if response
== gtk
.RESPONSE_OK
:
3779 filename
= dlg
.get_filename()
3781 exporter
= opml
.Exporter( filename
)
3782 if exporter
.write(self
.channels
):
3783 count
= len(self
.channels
)
3784 title
= N_('%(count)d subscription exported', '%(count)d subscriptions exported', count
) % {'count':count
}
3785 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3787 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3791 def on_itemImportChannels_activate(self
, widget
, *args
):
3792 if gpodder
.ui
.fremantle
:
3793 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3794 self
.config
.toplist_url
, \
3795 self
.config
.opml_url
, \
3796 self
.add_podcast_list
, \
3797 self
.on_itemAddChannel_activate
, \
3798 self
.on_download_subscriptions_from_mygpo
, \
3799 self
.show_text_edit_dialog
)
3801 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3802 add_urls_callback
=self
.add_podcast_list
)
3803 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3805 def on_homepage_activate(self
, widget
, *args
):
3806 util
.open_website(gpodder
.__url
__)
3808 def on_wiki_activate(self
, widget
, *args
):
3809 util
.open_website('http://gpodder.org/wiki/User_Manual')
3811 def on_bug_tracker_activate(self
, widget
, *args
):
3812 if gpodder
.ui
.maemo
:
3813 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3815 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3817 def on_item_support_activate(self
, widget
):
3818 util
.open_website('http://gpodder.org/donate')
3820 def on_itemAbout_activate(self
, widget
, *args
):
3821 if gpodder
.ui
.fremantle
:
3822 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3823 HeAboutDialog
.present(self
.main_window
,
3826 gpodder
.__version
__,
3827 _('A podcast client with focus on usability'),
3828 gpodder
.__copyright
__,
3830 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3831 'http://gpodder.org/donate')
3834 dlg
= gtk
.AboutDialog()
3835 dlg
.set_transient_for(self
.main_window
)
3836 dlg
.set_name('gPodder')
3837 dlg
.set_version(gpodder
.__version
__)
3838 dlg
.set_copyright(gpodder
.__copyright
__)
3839 dlg
.set_comments(_('A podcast client with focus on usability'))
3840 dlg
.set_website(gpodder
.__url
__)
3841 dlg
.set_translator_credits( _('translator-credits'))
3842 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3844 if gpodder
.ui
.desktop
:
3845 # For the "GUI" version, we add some more
3846 # items to the about dialog (credits and logo)
3849 'Thomas Perl <thp.io>',
3852 if os
.path
.exists(gpodder
.credits_file
):
3853 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3854 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3855 app_authors
+= credits
3857 dlg
.set_authors(app_authors
)
3859 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3861 dlg
.set_logo_icon_name('gpodder')
3865 def on_wNotebook_switch_page(self
, widget
, *args
):
3867 if gpodder
.ui
.maemo
:
3868 self
.tool_downloads
.set_active(page_num
== 1)
3869 page
= self
.wNotebook
.get_nth_page(page_num
)
3870 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3871 if page_num
== 0 and self
.active_channel
is not None:
3872 self
.set_title(self
.active_channel
.title
)
3874 self
.set_title(tab_label
)
3876 self
.play_or_download()
3877 self
.menuChannels
.set_sensitive(True)
3878 self
.menuSubscriptions
.set_sensitive(True)
3879 # The message area in the downloads tab should be hidden
3880 # when the user switches away from the downloads tab
3881 if self
.message_area
is not None:
3882 self
.message_area
.hide()
3883 self
.message_area
= None
3885 self
.menuChannels
.set_sensitive(False)
3886 self
.menuSubscriptions
.set_sensitive(False)
3887 if gpodder
.ui
.desktop
:
3888 self
.toolDownload
.set_sensitive(False)
3889 self
.toolPlay
.set_sensitive(False)
3890 self
.toolTransfer
.set_sensitive(False)
3891 self
.toolCancel
.set_sensitive(False)
3893 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3894 # double-click action of the podcast list or enter
3895 self
.treeChannels
.set_cursor(path
)
3897 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3898 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3900 if model
is not None and iter is not None:
3901 old_active_channel
= self
.active_channel
3902 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3904 if self
.active_channel
== old_active_channel
:
3907 if gpodder
.ui
.maemo
:
3908 self
.set_title(self
.active_channel
.title
)
3910 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3911 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3912 self
.itemEditChannel
.set_visible(False)
3913 self
.itemRemoveChannel
.set_visible(False)
3915 self
.itemEditChannel
.set_visible(True)
3916 self
.itemRemoveChannel
.set_visible(True)
3918 self
.active_channel
= None
3919 self
.itemEditChannel
.set_visible(False)
3920 self
.itemRemoveChannel
.set_visible(False)
3922 self
.update_episode_list_model()
3924 def on_btnEditChannel_clicked(self
, widget
, *args
):
3925 self
.on_itemEditChannel_activate( widget
, args
)
3927 def get_podcast_urls_from_selected_episodes(self
):
3928 """Get a set of podcast URLs based on the selected episodes"""
3929 return set(episode
.channel
.url
for episode
in \
3930 self
.get_selected_episodes())
3932 def get_selected_episodes(self
):
3933 """Get a list of selected episodes from treeAvailable"""
3934 selection
= self
.treeAvailable
.get_selection()
3935 model
, paths
= selection
.get_selected_rows()
3937 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3940 def on_transfer_selected_episodes(self
, widget
):
3941 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3943 def on_playback_selected_episodes(self
, widget
):
3944 self
.playback_episodes(self
.get_selected_episodes())
3946 def on_shownotes_selected_episodes(self
, widget
):
3947 episodes
= self
.get_selected_episodes()
3949 episode
= episodes
.pop(0)
3950 self
.show_episode_shownotes(episode
)
3952 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3954 def on_download_selected_episodes(self
, widget
):
3955 episodes
= self
.get_selected_episodes()
3956 self
.download_episode_list(episodes
)
3957 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3958 self
.play_or_download()
3960 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3961 """Double-click/enter action handler for treeAvailable"""
3962 # We should only have one one selected as it was double clicked!
3963 e
= self
.get_selected_episodes()[0]
3965 if (self
.config
.double_click_episode_action
== 'download'):
3966 # If the episode has already been downloaded and exists then play it
3967 if e
.was_downloaded(and_exists
=True):
3968 self
.playback_episodes(self
.get_selected_episodes())
3969 # else download it if it is not already downloading
3970 elif not self
.episode_is_downloading(e
):
3971 self
.download_episode_list([e
])
3972 self
.update_episode_list_icons([e
.url
])
3973 self
.play_or_download()
3974 elif (self
.config
.double_click_episode_action
== 'stream'):
3975 # If we happen to have downloaded this episode simple play it
3976 if e
.was_downloaded(and_exists
=True):
3977 self
.playback_episodes(self
.get_selected_episodes())
3978 # else if streaming is possible stream it
3979 elif self
.streaming_possible():
3980 self
.playback_episodes(self
.get_selected_episodes())
3982 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3983 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3985 # default action is to display show notes
3986 self
.on_shownotes_selected_episodes(widget
)
3988 def show_episode_shownotes(self
, episode
):
3989 if self
.episode_shownotes_window
is None:
3990 log('First-time use of episode window --- creating', sender
=self
)
3991 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3992 _download_episode_list
=self
.download_episode_list
, \
3993 _playback_episodes
=self
.playback_episodes
, \
3994 _delete_episode_list
=self
.delete_episode_list
, \
3995 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3996 _cancel_task_list
=self
.cancel_task_list
, \
3997 _episode_is_downloading
=self
.episode_is_downloading
, \
3998 _streaming_possible
=self
.streaming_possible())
3999 self
.episode_shownotes_window
.show(episode
)
4000 if self
.episode_is_downloading(episode
):
4001 self
.update_downloads_list()
4003 def restart_auto_update_timer(self
):
4004 if self
._auto
_update
_timer
_source
_id
is not None:
4005 log('Removing existing auto update timer.', sender
=self
)
4006 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
4007 self
._auto
_update
_timer
_source
_id
= None
4009 if self
.config
.auto_update_feeds
and \
4010 self
.config
.auto_update_frequency
:
4011 interval
= 60*1000*self
.config
.auto_update_frequency
4012 log('Setting up auto update timer with interval %d.', \
4013 self
.config
.auto_update_frequency
, sender
=self
)
4014 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
4015 interval
, self
._on
_auto
_update
_timer
)
4017 def _on_auto_update_timer(self
):
4018 log('Auto update timer fired.', sender
=self
)
4019 self
.update_feed_cache(force_update
=True)
4021 # Ask web service for sub changes (if enabled)
4022 self
.mygpo_client
.flush()
4026 def on_treeDownloads_row_activated(self
, widget
, *args
):
4027 # Use the standard way of working on the treeview
4028 selection
= self
.treeDownloads
.get_selection()
4029 (model
, paths
) = selection
.get_selected_rows()
4030 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
4032 for tree_row_reference
, task
in selected_tasks
:
4033 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
4034 task
.status
= task
.PAUSED
4035 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
4036 self
.download_queue_manager
.add_task(task
)
4037 self
.enable_download_list_update()
4038 elif task
.status
== task
.DONE
:
4039 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
4041 self
.play_or_download()
4043 # Update the tab title and downloads list
4044 self
.update_downloads_list()
4046 def on_item_cancel_download_activate(self
, widget
):
4047 if self
.wNotebook
.get_current_page() == 0:
4048 selection
= self
.treeAvailable
.get_selection()
4049 (model
, paths
) = selection
.get_selected_rows()
4050 urls
= [model
.get_value(model
.get_iter(path
), \
4051 self
.episode_list_model
.C_URL
) for path
in paths
]
4052 selected_tasks
= [task
for task
in self
.download_tasks_seen \
4053 if task
.url
in urls
]
4055 selection
= self
.treeDownloads
.get_selection()
4056 (model
, paths
) = selection
.get_selected_rows()
4057 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
4058 self
.download_status_model
.C_TASK
) for path
in paths
]
4059 self
.cancel_task_list(selected_tasks
)
4061 def on_btnCancelAll_clicked(self
, widget
, *args
):
4062 self
.cancel_task_list(self
.download_tasks_seen
)
4064 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
4065 episodes
= self
.get_selected_episodes()
4066 if len(episodes
) == 1:
4067 self
.delete_episode_list(episodes
, skip_locked
=False)
4069 self
.delete_episode_list(episodes
)
4071 def on_key_press(self
, widget
, event
):
4072 # Allow tab switching with Ctrl + PgUp/PgDown
4073 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
4074 if event
.keyval
== gtk
.keysyms
.Page_Up
:
4075 self
.wNotebook
.prev_page()
4077 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
4078 self
.wNotebook
.next_page()
4081 # After this code we only handle Maemo hardware keys,
4082 # so if we are not a Maemo app, we don't do anything
4083 if not gpodder
.ui
.maemo
:
4087 if event
.keyval
== gtk
.keysyms
.F7
: #plus
4089 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
4092 if diff
!= 0 and not self
.currently_updating
:
4093 selection
= self
.treeChannels
.get_selection()
4094 (model
, iter) = selection
.get_selected()
4095 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
4096 selection
.select_path(new_path
)
4097 self
.treeChannels
.set_cursor(new_path
)
4102 def on_iconify(self
):
4104 self
.gPodder
.set_skip_taskbar_hint(True)
4105 if self
.config
.minimize_to_tray
:
4106 self
.tray_icon
.set_visible(True)
4108 self
.gPodder
.set_skip_taskbar_hint(False)
4110 def on_uniconify(self
):
4112 self
.gPodder
.set_skip_taskbar_hint(False)
4113 if self
.config
.minimize_to_tray
:
4114 self
.tray_icon
.set_visible(False)
4116 self
.gPodder
.set_skip_taskbar_hint(False)
4118 def uniconify_main_window(self
):
4119 if self
.is_iconified():
4120 # We need to hide and then show the window in WMs like Metacity
4121 # or KWin4 to move the window to the active workspace
4122 # (see http://gpodder.org/bug/1125)
4125 self
.gPodder
.present()
4127 def iconify_main_window(self
):
4128 if not self
.is_iconified():
4129 self
.gPodder
.iconify()
4131 def update_podcasts_tab(self
):
4132 if len(self
.channels
):
4133 if gpodder
.ui
.fremantle
:
4134 self
.button_refresh
.set_title(_('Check for new episodes'))
4135 self
.button_refresh
.show()
4137 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
4139 if gpodder
.ui
.fremantle
:
4140 self
.button_refresh
.hide()
4142 self
.label2
.set_text(_('Podcasts'))
4144 @dbus.service
.method(gpodder
.dbus_interface
)
4145 def show_gui_window(self
):
4146 parent
= self
.get_dialog_parent()
4149 @dbus.service
.method(gpodder
.dbus_interface
)
4150 def subscribe_to_url(self
, url
):
4151 gPodderAddPodcast(self
.gPodder
,
4152 add_urls_callback
=self
.add_podcast_list
,
4155 @dbus.service
.method(gpodder
.dbus_interface
)
4156 def mark_episode_played(self
, filename
):
4157 if filename
is None:
4160 for channel
in self
.channels
:
4161 for episode
in channel
.get_all_episodes():
4162 fn
= episode
.local_filename(create
=False, check_only
=True)
4164 episode
.mark(is_played
=True)
4166 self
.update_episode_list_icons([episode
.url
])
4167 self
.update_podcast_list_model([episode
.channel
.url
])
4173 def main(options
=None):
4174 gobject
.threads_init()
4175 gobject
.set_application_name('gPodder')
4177 if gpodder
.ui
.maemo
:
4178 # Try to enable the custom icon theme for gPodder on Maemo
4179 settings
= gtk
.settings_get_default()
4180 settings
.set_string_property('gtk-icon-theme-name', \
4181 'gpodder', __file__
)
4182 # Extend the search path for the optified icon theme (Maemo 5)
4183 icon_theme
= gtk
.icon_theme_get_default()
4184 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
4186 gtk
.window_set_default_icon_name('gpodder')
4187 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
4190 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
4191 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
4193 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
4194 except dbus
.exceptions
.DBusException
, dbe
:
4195 log('Warning: Cannot get "on the bus".', traceback
=True)
4196 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
4197 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
4198 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
4199 dlg
.set_title('gPodder')
4204 util
.make_directory(gpodder
.home
)
4205 gpodder
.load_plugins()
4207 config
= UIConfig(gpodder
.config_file
)
4209 # Load hook modules and install the hook manager globally
4210 # if modules have been found an instantiated by the manager
4211 user_hooks
= hooks
.HookManager()
4212 if user_hooks
.has_modules():
4213 gpodder
.user_hooks
= user_hooks
4215 if gpodder
.ui
.diablo
:
4216 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4217 # folder exists there (allow moving "gpodder" between SD cards or USB)
4218 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4219 if not os
.path
.exists(config
.download_dir
):
4220 log('Downloads might have been moved. Trying to locate them...')
4221 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4222 dir = os
.path
.join(basedir
, 'gpodder')
4223 if os
.path
.exists(dir):
4224 log('Downloads found in: %s', dir)
4225 config
.download_dir
= dir
4228 log('Downloads NOT FOUND in %s', dir)
4230 if config
.enable_fingerscroll
:
4231 BuilderWidget
.use_fingerscroll
= True
4233 config
.mygpo_device_type
= util
.detect_device_type()
4235 gp
= gPodder(bus_name
, config
)
4238 if options
.subscribe
:
4239 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4242 # handle "subscribe to podcast" events from firefox
4243 if platform
.system() == 'Darwin':
4244 from gpodder
import gpodderosx
4245 gpodderosx
.register_handlers(gp
)
4246 # end mac OS X stuff