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_('%d partial file', '%d partial files', 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_('%d active', '%d active', downloading
) % downloading
)
1382 s
.append(N_('%d failed', '%d failed', failed
) % failed
)
1384 s
.append(N_('%d queued', '%d queued', queued
) % 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_('%d active', '%d active', downloading
+queued
) % (downloading
+queued
))
1397 self
.button_downloads
.set_value(N_('%d failed', '%d failed', failed
) % failed
)
1399 self
.button_downloads
.set_value(N_('%d paused', '%d paused', paused
) % 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 %d file', 'downloading %d files', 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_('%d more episode', '%d more episodes', more_episodes
) % 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 %d episode', 'Opening %d episodes', 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
.fremantle
:
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_('%d action processed', '%d actions processed', idx
) % 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 %d new episode.', 'Downloading %d new episodes.', 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 %d new episode.', 'Downloading %d new episodes.', 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_('%d new episode added to download list.', '%d new episodes added to download list.', 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_('%d new episode available', '%d new episodes available', 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 %d feed...', 'Updating %d feeds...', 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()
3066 elif self
.config
.on_quit_ask
or downloading
:
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')
3081 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3083 message
= _('Do you really want to quit gPodder now?')
3085 dialog
.set_title(title
)
3086 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
3088 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
3089 dialog
.vbox
.pack_start(cb_ask
)
3092 quit_button
.grab_focus()
3093 result
= dialog
.run()
3096 if result
== gtk
.RESPONSE_CLOSE
:
3097 if not downloading
and cb_ask
.get_active() == True:
3098 self
.config
.on_quit_ask
= False
3099 self
.close_gpodder()
3101 self
.close_gpodder()
3105 def close_gpodder(self
):
3106 """ clean everything and exit properly
3109 if self
.save_channels_opml():
3110 pass # FIXME: Add mygpo synchronization here
3112 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
3116 if self
.tray_icon
is not None:
3117 self
.tray_icon
.set_visible(False)
3119 # Notify all tasks to to carry out any clean-up actions
3120 self
.download_status_model
.tell_all_tasks_to_quit()
3122 while gtk
.events_pending():
3123 gtk
.main_iteration(False)
3130 def get_expired_episodes(self
):
3131 for channel
in self
.channels
:
3132 for episode
in channel
.get_downloaded_episodes():
3133 # Never consider locked episodes as old
3134 if episode
.is_locked
:
3137 # Never consider fresh episodes as old
3138 if episode
.age_in_days() < self
.config
.episode_old_age
:
3141 # Do not delete played episodes (except if configured)
3142 if episode
.is_played
:
3143 if not self
.config
.auto_remove_played_episodes
:
3146 # Do not delete unplayed episodes (except if configured)
3147 if not episode
.is_played
:
3148 if not self
.config
.auto_remove_unplayed_episodes
:
3153 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3158 episodes
= [e
for e
in episodes
if not e
.is_locked
]
3161 title
= _('Episodes are locked')
3162 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3163 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3166 count
= len(episodes
)
3167 title
= N_('Delete %d episode?', 'Delete %d episodes?', count
) % count
3168 message
= _('Deleting episodes removes downloaded files.')
3170 if gpodder
.ui
.fremantle
:
3171 message
= '\n'.join([title
, message
])
3173 if confirm
and not self
.show_confirmation(message
, title
):
3176 progress
= ProgressIndicator(_('Deleting episodes'), \
3177 _('Please wait while episodes are deleted'), \
3178 parent
=self
.get_dialog_parent())
3180 def finish_deletion(episode_urls
, channel_urls
):
3181 progress
.on_finished()
3183 # Episodes have been deleted - persist the database
3186 self
.update_episode_list_icons(episode_urls
)
3187 self
.update_podcast_list_model(channel_urls
)
3188 self
.play_or_download()
3191 episode_urls
= set()
3192 channel_urls
= set()
3194 episodes_status_update
= []
3195 for idx
, episode
in enumerate(episodes
):
3196 progress
.on_progress(float(idx
)/float(len(episodes
)))
3197 if episode
.is_locked
and skip_locked
:
3198 log('Not deleting episode (is locked): %s', episode
.title
)
3200 log('Deleting episode: %s', episode
.title
)
3201 progress
.on_message(episode
.title
)
3202 episode
.delete_from_disk()
3203 episode_urls
.add(episode
.url
)
3204 channel_urls
.add(episode
.channel
.url
)
3205 episodes_status_update
.append(episode
)
3207 # Tell the shownotes window that we have removed the episode
3208 if self
.episode_shownotes_window
is not None and \
3209 self
.episode_shownotes_window
.episode
is not None and \
3210 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3211 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3213 # Notify the web service about the status update + upload
3214 self
.mygpo_client
.on_delete(episodes_status_update
)
3215 self
.mygpo_client
.flush()
3217 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3219 threading
.Thread(target
=thread_proc
).start()
3223 def on_itemRemoveOldEpisodes_activate(self
, widget
):
3224 self
.show_delete_episodes_window()
3226 def show_delete_episodes_window(self
, channel
=None):
3227 """Offer deletion of episodes
3229 If channel is None, offer deletion of all episodes.
3230 Otherwise only offer deletion of episodes in the channel.
3232 if gpodder
.ui
.maemo
:
3234 ('maemo_remove_markup', None, None, _('Episode')),
3238 ('title_markup', None, None, _('Episode')),
3239 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3240 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3241 ('played_prop', None, None, _('Status')),
3242 ('age_prop', 'age_int_prop', gobject
.TYPE_INT
, _('Downloaded')),
3245 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
3246 selection_buttons
= {
3247 _('Select played'): lambda episode
: episode
.is_played
,
3248 _('Select finished'): lambda episode
: episode
.is_finished(),
3249 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3252 instructions
= _('Select the episodes you want to delete:')
3255 channels
= self
.channels
3257 channels
= [channel
]
3260 for channel
in channels
:
3261 for episode
in channel
.get_downloaded_episodes():
3262 # Disallow deletion of locked episodes that still exist
3263 if not episode
.is_locked
or not episode
.file_exists():
3264 episodes
.append(episode
)
3266 selected
= [e
for e
in episodes
if episode
.is_played
or not episode
.file_exists()]
3268 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3269 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3270 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3271 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3272 show_episode_shownotes
=self
.show_episode_shownotes
)
3274 def on_selected_episodes_status_changed(self
):
3275 # The order of the updates here is important! When "All episodes" is
3276 # selected, the update of the podcast list model depends on the episode
3277 # list selection to determine which podcasts are affected. Updating
3278 # the episode list could remove the selection if a filter is active.
3279 self
.update_podcast_list_model(selected
=True)
3280 self
.update_episode_list_icons(selected
=True)
3283 def mark_selected_episodes_new(self
):
3284 for episode
in self
.get_selected_episodes():
3286 self
.on_selected_episodes_status_changed()
3288 def mark_selected_episodes_old(self
):
3289 for episode
in self
.get_selected_episodes():
3291 self
.on_selected_episodes_status_changed()
3293 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3294 for episode
in self
.get_selected_episodes():
3296 episode
.mark(is_played
=not episode
.is_played
)
3298 episode
.mark(is_played
=new_value
)
3299 self
.on_selected_episodes_status_changed()
3301 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3302 for episode
in self
.get_selected_episodes():
3304 episode
.mark(is_locked
=not episode
.is_locked
)
3306 episode
.mark(is_locked
=new_value
)
3307 self
.on_selected_episodes_status_changed()
3309 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3310 if self
.active_channel
is None:
3313 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3314 self
.active_channel
.update_channel_lock()
3316 for episode
in self
.active_channel
.get_all_episodes():
3317 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3319 self
.update_podcast_list_model(selected
=True)
3320 self
.update_episode_list_icons(all
=True)
3322 def on_itemUpdateChannel_activate(self
, widget
=None):
3323 if self
.active_channel
is None:
3324 title
= _('No podcast selected')
3325 message
= _('Please select a podcast in the podcasts list to update.')
3326 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3329 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3330 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3331 self
.update_feed_cache()
3333 self
.update_feed_cache(channels
=[self
.active_channel
])
3335 def on_itemUpdate_activate(self
, widget
=None):
3336 # Check if we have outstanding subscribe/unsubscribe actions
3337 if self
.on_add_remove_podcasts_mygpo():
3338 log('Update cancelled (received server changes)', sender
=self
)
3342 self
.update_feed_cache()
3344 gPodderWelcome(self
.gPodder
,
3345 center_on_widget
=self
.gPodder
,
3346 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3347 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3349 def download_episode_list_paused(self
, episodes
):
3350 self
.download_episode_list(episodes
, True)
3352 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3353 enable_update
= False
3355 for episode
in episodes
:
3356 log('Downloading episode: %s', episode
.title
, sender
= self
)
3357 if not episode
.was_downloaded(and_exists
=True):
3359 for task
in self
.download_tasks_seen
:
3360 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3361 self
.download_queue_manager
.add_task(task
, force_start
)
3362 enable_update
= True
3370 task
= download
.DownloadTask(episode
, self
.config
)
3371 except Exception, e
:
3372 d
= {'episode': episode
.title
, 'message': str(e
)}
3373 message
= _('Download error while downloading %(episode)s: %(message)s')
3374 self
.show_message(message
% d
, _('Download error'), important
=True)
3375 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3379 task
.status
= task
.PAUSED
3381 self
.mygpo_client
.on_download([task
.episode
])
3382 self
.download_queue_manager
.add_task(task
, force_start
)
3384 self
.download_status_model
.register_task(task
)
3385 enable_update
= True
3388 self
.enable_download_list_update()
3390 # Flush updated episode status
3391 self
.mygpo_client
.flush()
3393 def cancel_task_list(self
, tasks
):
3398 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3399 task
.status
= task
.CANCELLED
3400 elif task
.status
== task
.PAUSED
:
3401 task
.status
= task
.CANCELLED
3402 # Call run, so the partial file gets deleted
3405 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3406 self
.play_or_download()
3408 # Update the tab title and downloads list
3409 self
.update_downloads_list()
3411 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3412 if gpodder
.ui
.maemo
:
3414 ('maemo_markup', None, None, _('Episode')),
3416 show_notification
= notification
3419 ('title_markup', None, None, _('Episode')),
3420 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3421 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3423 show_notification
= False
3425 instructions
= _('Select the episodes you want to download:')
3427 if self
.new_episodes_window
is not None:
3428 self
.new_episodes_window
.main_window
.destroy()
3429 self
.new_episodes_window
= None
3431 def download_episodes_callback(episodes
):
3432 self
.new_episodes_window
= None
3433 self
.download_episode_list(episodes
)
3435 if selected
is None:
3436 # Select all by default
3437 selected
= [True]*len(episodes
)
3439 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3440 title
=_('New episodes available'), \
3441 instructions
=instructions
, \
3442 episodes
=episodes
, \
3444 selected
=selected
, \
3445 stock_ok_button
= 'gpodder-download', \
3446 callback
=download_episodes_callback
, \
3447 remove_callback
=lambda e
: e
.mark_old(), \
3448 remove_action
=_('Mark as old'), \
3449 remove_finished
=self
.episode_new_status_changed
, \
3450 _config
=self
.config
, \
3451 show_notification
=show_notification
, \
3452 show_episode_shownotes
=self
.show_episode_shownotes
)
3454 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3455 if not self
.offer_new_episodes():
3456 self
.show_message(_('Please check for new episodes later.'), \
3457 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3459 def get_new_episodes(self
, channels
=None):
3460 if channels
is None:
3461 channels
= self
.channels
3463 for channel
in channels
:
3464 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3465 episodes
.append(episode
)
3469 @dbus.service
.method(gpodder
.dbus_interface
)
3470 def start_device_synchronization(self
):
3471 """Public D-Bus API for starting Device sync (Desktop only)
3473 This method can be called to initiate a synchronization with
3474 a configured protable media player. This only works for the
3475 Desktop version of gPodder and does nothing on Maemo.
3477 if gpodder
.ui
.desktop
:
3478 self
.on_sync_to_ipod_activate(None)
3483 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3484 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3486 def commit_changes_to_database(self
):
3487 """This will be called after the sync process is finished"""
3490 def on_cleanup_ipod_activate(self
, widget
, *args
):
3491 self
.sync_ui
.on_cleanup_device()
3493 def on_manage_device_playlist(self
, widget
):
3494 self
.sync_ui
.on_manage_device_playlist()
3496 def show_hide_tray_icon(self
):
3497 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3498 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3499 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3500 self
.tray_icon
.set_visible(False)
3502 self
.tray_icon
= None
3504 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3505 self
.tray_icon
.set_visible(self
.is_iconified())
3506 elif self
.tray_icon
:
3507 self
.tray_icon
.set_visible(True)
3509 def on_itemShowAllEpisodes_activate(self
, widget
):
3510 self
.config
.podcast_list_view_all
= widget
.get_active()
3512 def on_itemShowToolbar_activate(self
, widget
):
3513 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3515 def on_itemShowDescription_activate(self
, widget
):
3516 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3518 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3519 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3520 if self
.config
.podcast_list_hide_boring
:
3521 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3523 self
.podcast_list_model
.set_view_mode(-1)
3525 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3527 if current
== self
.item_view_podcasts_all
:
3528 self
.podcast_list_model
.set_view_mode(-1)
3529 elif current
== self
.item_view_podcasts_downloaded
:
3530 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3531 elif current
== self
.item_view_podcasts_unplayed
:
3532 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3534 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3536 def on_item_view_episodes_changed(self
, radioaction
, current
):
3537 if current
== self
.item_view_episodes_all
:
3538 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_ALL
3539 elif current
== self
.item_view_episodes_undeleted
:
3540 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNDELETED
3541 elif current
== self
.item_view_episodes_downloaded
:
3542 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_DOWNLOADED
3543 elif current
== self
.item_view_episodes_unplayed
:
3544 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNPLAYED
3546 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3548 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3549 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3551 def update_item_device( self
):
3552 if not gpodder
.ui
.fremantle
:
3553 if self
.config
.device_type
!= 'none':
3554 self
.itemDevice
.set_visible(True)
3555 self
.itemDevice
.label
= self
.get_device_name()
3557 self
.itemDevice
.set_visible(False)
3559 def properties_closed( self
):
3560 self
.preferences_dialog
= None
3561 self
.show_hide_tray_icon()
3562 self
.update_item_device()
3563 if gpodder
.ui
.maemo
:
3564 selection
= self
.treeAvailable
.get_selection()
3565 if self
.config
.maemo_enable_gestures
or \
3566 self
.config
.enable_fingerscroll
:
3567 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3569 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3571 def on_itemPreferences_activate(self
, widget
, *args
):
3572 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3573 _config
=self
.config
, \
3574 callback_finished
=self
.properties_closed
, \
3575 user_apps_reader
=self
.user_apps_reader
, \
3576 parent_window
=self
.main_window
, \
3577 mygpo_client
=self
.mygpo_client
, \
3578 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3580 # Initial message to relayout window (in case it's opened in portrait mode
3581 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3583 def on_itemDependencies_activate(self
, widget
):
3584 gPodderDependencyManager(self
.gPodder
)
3586 def on_goto_mygpo(self
, widget
):
3587 self
.mygpo_client
.open_website()
3589 def on_download_subscriptions_from_mygpo(self
, action
=None):
3590 title
= _('Login to gpodder.net')
3591 message
= _('Please login to download your subscriptions.')
3592 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3593 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3597 self
.config
.mygpo_username
= username
3598 self
.config
.mygpo_password
= password
3600 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3601 custom_title
=_('Subscriptions on gpodder.net'), \
3602 add_urls_callback
=self
.add_podcast_list
, \
3603 hide_url_entry
=True)
3605 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3606 # we do not have to hardcode the URL here
3607 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3608 url
= util
.url_add_authentication(OPML_URL
, \
3609 self
.config
.mygpo_username
, \
3610 self
.config
.mygpo_password
)
3611 dir.download_opml_file(url
)
3613 def on_mygpo_settings_activate(self
, action
=None):
3614 # This dialog is only used for Maemo 4
3615 if not gpodder
.ui
.diablo
:
3618 settings
= MygPodderSettings(self
.main_window
, \
3619 config
=self
.config
, \
3620 mygpo_client
=self
.mygpo_client
, \
3621 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3623 def on_itemAddChannel_activate(self
, widget
=None):
3624 gPodderAddPodcast(self
.gPodder
, \
3625 add_urls_callback
=self
.add_podcast_list
)
3627 def on_itemEditChannel_activate(self
, widget
, *args
):
3628 if self
.active_channel
is None:
3629 title
= _('No podcast selected')
3630 message
= _('Please select a podcast in the podcasts list to edit.')
3631 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3634 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3635 gPodderChannel(self
.main_window
, \
3636 channel
=self
.active_channel
, \
3637 callback_closed
=callback_closed
, \
3638 cover_downloader
=self
.cover_downloader
)
3640 def on_itemMassUnsubscribe_activate(self
, item
=None):
3642 ('title', None, None, _('Podcast')),
3645 # We're abusing the Episode Selector for selecting Podcasts here,
3646 # but it works and looks good, so why not? -- thp
3647 gPodderEpisodeSelector(self
.main_window
, \
3648 title
=_('Remove podcasts'), \
3649 instructions
=_('Select the podcast you want to remove.'), \
3650 episodes
=self
.channels
, \
3652 size_attribute
=None, \
3653 stock_ok_button
=_('Remove'), \
3654 callback
=self
.remove_podcast_list
, \
3655 _config
=self
.config
)
3657 def remove_podcast_list(self
, channels
, confirm
=True):
3659 log('No podcasts selected for deletion', sender
=self
)
3662 if len(channels
) == 1:
3663 title
= _('Removing podcast')
3664 info
= _('Please wait while the podcast is removed')
3665 message
= _('Do you really want to remove this podcast and its episodes?')
3667 title
= _('Removing podcasts')
3668 info
= _('Please wait while the podcasts are removed')
3669 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3671 if confirm
and not self
.show_confirmation(message
, title
):
3674 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3676 def finish_deletion(select_url
):
3677 # Upload subscription list changes to the web service
3678 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3680 # Re-load the channels and select the desired new channel
3681 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3682 progress
.on_finished()
3683 self
.update_podcasts_tab()
3688 for idx
, channel
in enumerate(channels
):
3689 # Update the UI for correct status messages
3690 progress
.on_progress(float(idx
)/float(len(channels
)))
3691 progress
.on_message(channel
.title
)
3693 # Delete downloaded episodes
3694 channel
.remove_downloaded()
3696 # cancel any active downloads from this channel
3697 for episode
in channel
.get_all_episodes():
3698 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3701 if len(channels
) == 1:
3702 # get the URL of the podcast we want to select next
3703 if channel
in self
.channels
:
3704 position
= self
.channels
.index(channel
)
3708 if position
== len(self
.channels
)-1:
3709 # this is the last podcast, so select the URL
3710 # of the item before this one (i.e. the "new last")
3711 select_url
= self
.channels
[position
-1].url
3713 # there is a podcast after the deleted one, so
3714 # we simply select the one that comes after it
3715 select_url
= self
.channels
[position
+1].url
3717 # Remove the channel and clean the database entries
3719 self
.channels
.remove(channel
)
3721 # Clean up downloads and download directories
3722 self
.clean_up_downloads()
3724 self
.channel_list_changed
= True
3725 self
.save_channels_opml()
3727 # The remaining stuff is to be done in the GTK main thread
3728 util
.idle_add(finish_deletion
, select_url
)
3730 threading
.Thread(target
=thread_proc
).start()
3732 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3733 if self
.active_channel
is None:
3734 title
= _('No podcast selected')
3735 message
= _('Please select a podcast in the podcasts list to remove.')
3736 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3739 self
.remove_podcast_list([self
.active_channel
])
3741 def get_opml_filter(self
):
3742 filter = gtk
.FileFilter()
3743 filter.add_pattern('*.opml')
3744 filter.add_pattern('*.xml')
3745 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3748 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3749 if filename
is None:
3750 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3751 # FIXME: Hildonization on Fremantle
3752 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3753 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3754 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3755 elif gpodder
.ui
.diablo
:
3756 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3757 dlg
.set_filter(self
.get_opml_filter())
3758 response
= dlg
.run()
3760 if response
== gtk
.RESPONSE_OK
:
3761 filename
= dlg
.get_filename()
3764 if filename
is not None:
3765 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3766 custom_title
=_('Import podcasts from OPML file'), \
3767 add_urls_callback
=self
.add_podcast_list
, \
3768 hide_url_entry
=True)
3769 dir.download_opml_file(filename
)
3771 def on_itemExportChannels_activate(self
, widget
, *args
):
3772 if not self
.channels
:
3773 title
= _('Nothing to export')
3774 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3775 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3778 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3779 # FIXME: Hildonization on Fremantle
3780 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3781 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3782 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3783 elif gpodder
.ui
.diablo
:
3784 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3785 dlg
.set_filter(self
.get_opml_filter())
3786 response
= dlg
.run()
3787 if response
== gtk
.RESPONSE_OK
:
3788 filename
= dlg
.get_filename()
3790 exporter
= opml
.Exporter( filename
)
3791 if exporter
.write(self
.channels
):
3792 count
= len(self
.channels
)
3793 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3794 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3796 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3800 def on_itemImportChannels_activate(self
, widget
, *args
):
3801 if gpodder
.ui
.fremantle
:
3802 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3803 self
.config
.toplist_url
, \
3804 self
.config
.opml_url
, \
3805 self
.add_podcast_list
, \
3806 self
.on_itemAddChannel_activate
, \
3807 self
.on_download_subscriptions_from_mygpo
, \
3808 self
.show_text_edit_dialog
)
3810 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3811 add_urls_callback
=self
.add_podcast_list
)
3812 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3814 def on_homepage_activate(self
, widget
, *args
):
3815 util
.open_website(gpodder
.__url
__)
3817 def on_wiki_activate(self
, widget
, *args
):
3818 util
.open_website('http://gpodder.org/wiki/User_Manual')
3820 def on_bug_tracker_activate(self
, widget
, *args
):
3821 if gpodder
.ui
.maemo
:
3822 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3824 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3826 def on_item_support_activate(self
, widget
):
3827 util
.open_website('http://gpodder.org/donate')
3829 def on_itemAbout_activate(self
, widget
, *args
):
3830 if gpodder
.ui
.fremantle
:
3831 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3832 HeAboutDialog
.present(self
.main_window
,
3835 gpodder
.__version
__,
3836 _('A podcast client with focus on usability'),
3837 gpodder
.__copyright
__,
3839 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3840 'http://gpodder.org/donate')
3843 dlg
= gtk
.AboutDialog()
3844 dlg
.set_transient_for(self
.main_window
)
3845 dlg
.set_name('gPodder')
3846 dlg
.set_version(gpodder
.__version
__)
3847 dlg
.set_copyright(gpodder
.__copyright
__)
3848 dlg
.set_comments(_('A podcast client with focus on usability'))
3849 dlg
.set_website(gpodder
.__url
__)
3850 dlg
.set_translator_credits( _('translator-credits'))
3851 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3853 if gpodder
.ui
.desktop
:
3854 # For the "GUI" version, we add some more
3855 # items to the about dialog (credits and logo)
3858 'Thomas Perl <thpinfo.com>',
3861 if os
.path
.exists(gpodder
.credits_file
):
3862 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3863 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3864 app_authors
+= credits
3866 dlg
.set_authors(app_authors
)
3868 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3870 dlg
.set_logo_icon_name('gpodder')
3874 def on_wNotebook_switch_page(self
, widget
, *args
):
3876 if gpodder
.ui
.maemo
:
3877 self
.tool_downloads
.set_active(page_num
== 1)
3878 page
= self
.wNotebook
.get_nth_page(page_num
)
3879 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3880 if page_num
== 0 and self
.active_channel
is not None:
3881 self
.set_title(self
.active_channel
.title
)
3883 self
.set_title(tab_label
)
3885 self
.play_or_download()
3886 self
.menuChannels
.set_sensitive(True)
3887 self
.menuSubscriptions
.set_sensitive(True)
3888 # The message area in the downloads tab should be hidden
3889 # when the user switches away from the downloads tab
3890 if self
.message_area
is not None:
3891 self
.message_area
.hide()
3892 self
.message_area
= None
3894 self
.menuChannels
.set_sensitive(False)
3895 self
.menuSubscriptions
.set_sensitive(False)
3896 if gpodder
.ui
.desktop
:
3897 self
.toolDownload
.set_sensitive(False)
3898 self
.toolPlay
.set_sensitive(False)
3899 self
.toolTransfer
.set_sensitive(False)
3900 self
.toolCancel
.set_sensitive(False)
3902 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3903 # double-click action of the podcast list or enter
3904 self
.treeChannels
.set_cursor(path
)
3906 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3907 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3909 if model
is not None and iter is not None:
3910 old_active_channel
= self
.active_channel
3911 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3913 if self
.active_channel
== old_active_channel
:
3916 if gpodder
.ui
.maemo
:
3917 self
.set_title(self
.active_channel
.title
)
3919 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3920 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3921 self
.itemEditChannel
.set_visible(False)
3922 self
.itemRemoveChannel
.set_visible(False)
3924 self
.itemEditChannel
.set_visible(True)
3925 self
.itemRemoveChannel
.set_visible(True)
3927 self
.active_channel
= None
3928 self
.itemEditChannel
.set_visible(False)
3929 self
.itemRemoveChannel
.set_visible(False)
3931 self
.update_episode_list_model()
3933 def on_btnEditChannel_clicked(self
, widget
, *args
):
3934 self
.on_itemEditChannel_activate( widget
, args
)
3936 def get_podcast_urls_from_selected_episodes(self
):
3937 """Get a set of podcast URLs based on the selected episodes"""
3938 return set(episode
.channel
.url
for episode
in \
3939 self
.get_selected_episodes())
3941 def get_selected_episodes(self
):
3942 """Get a list of selected episodes from treeAvailable"""
3943 selection
= self
.treeAvailable
.get_selection()
3944 model
, paths
= selection
.get_selected_rows()
3946 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3949 def on_transfer_selected_episodes(self
, widget
):
3950 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3952 def on_playback_selected_episodes(self
, widget
):
3953 self
.playback_episodes(self
.get_selected_episodes())
3955 def on_shownotes_selected_episodes(self
, widget
):
3956 episodes
= self
.get_selected_episodes()
3958 episode
= episodes
.pop(0)
3959 self
.show_episode_shownotes(episode
)
3961 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3963 def on_download_selected_episodes(self
, widget
):
3964 episodes
= self
.get_selected_episodes()
3965 self
.download_episode_list(episodes
)
3966 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3967 self
.play_or_download()
3969 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3970 """Double-click/enter action handler for treeAvailable"""
3971 # We should only have one one selected as it was double clicked!
3972 e
= self
.get_selected_episodes()[0]
3974 if (self
.config
.double_click_episode_action
== 'download'):
3975 # If the episode has already been downloaded and exists then play it
3976 if e
.was_downloaded(and_exists
=True):
3977 self
.playback_episodes(self
.get_selected_episodes())
3978 # else download it if it is not already downloading
3979 elif not self
.episode_is_downloading(e
):
3980 self
.download_episode_list([e
])
3981 self
.update_episode_list_icons([e
.url
])
3982 self
.play_or_download()
3983 elif (self
.config
.double_click_episode_action
== 'stream'):
3984 # If we happen to have downloaded this episode simple play it
3985 if e
.was_downloaded(and_exists
=True):
3986 self
.playback_episodes(self
.get_selected_episodes())
3987 # else if streaming is possible stream it
3988 elif self
.streaming_possible():
3989 self
.playback_episodes(self
.get_selected_episodes())
3991 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3992 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3994 # default action is to display show notes
3995 self
.on_shownotes_selected_episodes(widget
)
3997 def show_episode_shownotes(self
, episode
):
3998 if self
.episode_shownotes_window
is None:
3999 log('First-time use of episode window --- creating', sender
=self
)
4000 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
4001 _download_episode_list
=self
.download_episode_list
, \
4002 _playback_episodes
=self
.playback_episodes
, \
4003 _delete_episode_list
=self
.delete_episode_list
, \
4004 _episode_list_status_changed
=self
.episode_list_status_changed
, \
4005 _cancel_task_list
=self
.cancel_task_list
, \
4006 _episode_is_downloading
=self
.episode_is_downloading
, \
4007 _streaming_possible
=self
.streaming_possible())
4008 self
.episode_shownotes_window
.show(episode
)
4009 if self
.episode_is_downloading(episode
):
4010 self
.update_downloads_list()
4012 def restart_auto_update_timer(self
):
4013 if self
._auto
_update
_timer
_source
_id
is not None:
4014 log('Removing existing auto update timer.', sender
=self
)
4015 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
4016 self
._auto
_update
_timer
_source
_id
= None
4018 if self
.config
.auto_update_feeds
and \
4019 self
.config
.auto_update_frequency
:
4020 interval
= 60*1000*self
.config
.auto_update_frequency
4021 log('Setting up auto update timer with interval %d.', \
4022 self
.config
.auto_update_frequency
, sender
=self
)
4023 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
4024 interval
, self
._on
_auto
_update
_timer
)
4026 def _on_auto_update_timer(self
):
4027 log('Auto update timer fired.', sender
=self
)
4028 self
.update_feed_cache(force_update
=True)
4030 # Ask web service for sub changes (if enabled)
4031 self
.mygpo_client
.flush()
4035 def on_treeDownloads_row_activated(self
, widget
, *args
):
4036 # Use the standard way of working on the treeview
4037 selection
= self
.treeDownloads
.get_selection()
4038 (model
, paths
) = selection
.get_selected_rows()
4039 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
4041 for tree_row_reference
, task
in selected_tasks
:
4042 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
4043 task
.status
= task
.PAUSED
4044 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
4045 self
.download_queue_manager
.add_task(task
)
4046 self
.enable_download_list_update()
4047 elif task
.status
== task
.DONE
:
4048 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
4050 self
.play_or_download()
4052 # Update the tab title and downloads list
4053 self
.update_downloads_list()
4055 def on_item_cancel_download_activate(self
, widget
):
4056 if self
.wNotebook
.get_current_page() == 0:
4057 selection
= self
.treeAvailable
.get_selection()
4058 (model
, paths
) = selection
.get_selected_rows()
4059 urls
= [model
.get_value(model
.get_iter(path
), \
4060 self
.episode_list_model
.C_URL
) for path
in paths
]
4061 selected_tasks
= [task
for task
in self
.download_tasks_seen \
4062 if task
.url
in urls
]
4064 selection
= self
.treeDownloads
.get_selection()
4065 (model
, paths
) = selection
.get_selected_rows()
4066 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
4067 self
.download_status_model
.C_TASK
) for path
in paths
]
4068 self
.cancel_task_list(selected_tasks
)
4070 def on_btnCancelAll_clicked(self
, widget
, *args
):
4071 self
.cancel_task_list(self
.download_tasks_seen
)
4073 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
4074 episodes
= self
.get_selected_episodes()
4075 if len(episodes
) == 1:
4076 self
.delete_episode_list(episodes
, skip_locked
=False)
4078 self
.delete_episode_list(episodes
)
4080 def on_key_press(self
, widget
, event
):
4081 # Allow tab switching with Ctrl + PgUp/PgDown
4082 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
4083 if event
.keyval
== gtk
.keysyms
.Page_Up
:
4084 self
.wNotebook
.prev_page()
4086 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
4087 self
.wNotebook
.next_page()
4090 # After this code we only handle Maemo hardware keys,
4091 # so if we are not a Maemo app, we don't do anything
4092 if not gpodder
.ui
.maemo
:
4096 if event
.keyval
== gtk
.keysyms
.F7
: #plus
4098 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
4101 if diff
!= 0 and not self
.currently_updating
:
4102 selection
= self
.treeChannels
.get_selection()
4103 (model
, iter) = selection
.get_selected()
4104 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
4105 selection
.select_path(new_path
)
4106 self
.treeChannels
.set_cursor(new_path
)
4111 def on_iconify(self
):
4113 self
.gPodder
.set_skip_taskbar_hint(True)
4114 if self
.config
.minimize_to_tray
:
4115 self
.tray_icon
.set_visible(True)
4117 self
.gPodder
.set_skip_taskbar_hint(False)
4119 def on_uniconify(self
):
4121 self
.gPodder
.set_skip_taskbar_hint(False)
4122 if self
.config
.minimize_to_tray
:
4123 self
.tray_icon
.set_visible(False)
4125 self
.gPodder
.set_skip_taskbar_hint(False)
4127 def uniconify_main_window(self
):
4128 if self
.is_iconified():
4129 # We need to hide and then show the window in WMs like Metacity
4130 # or KWin4 to move the window to the active workspace
4131 # (see http://gpodder.org/bug/1125)
4134 self
.gPodder
.present()
4136 def iconify_main_window(self
):
4137 if not self
.is_iconified():
4138 self
.gPodder
.iconify()
4140 def update_podcasts_tab(self
):
4141 if len(self
.channels
):
4142 if gpodder
.ui
.fremantle
:
4143 self
.button_refresh
.set_title(_('Check for new episodes'))
4144 self
.button_refresh
.show()
4146 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
4148 if gpodder
.ui
.fremantle
:
4149 self
.button_refresh
.hide()
4151 self
.label2
.set_text(_('Podcasts'))
4153 @dbus.service
.method(gpodder
.dbus_interface
)
4154 def show_gui_window(self
):
4155 parent
= self
.get_dialog_parent()
4158 @dbus.service
.method(gpodder
.dbus_interface
)
4159 def subscribe_to_url(self
, url
):
4160 gPodderAddPodcast(self
.gPodder
,
4161 add_urls_callback
=self
.add_podcast_list
,
4164 @dbus.service
.method(gpodder
.dbus_interface
)
4165 def mark_episode_played(self
, filename
):
4166 if filename
is None:
4169 for channel
in self
.channels
:
4170 for episode
in channel
.get_all_episodes():
4171 fn
= episode
.local_filename(create
=False, check_only
=True)
4173 episode
.mark(is_played
=True)
4175 self
.update_episode_list_icons([episode
.url
])
4176 self
.update_podcast_list_model([episode
.channel
.url
])
4182 def main(options
=None):
4183 gobject
.threads_init()
4184 gobject
.set_application_name('gPodder')
4186 if gpodder
.ui
.maemo
:
4187 # Try to enable the custom icon theme for gPodder on Maemo
4188 settings
= gtk
.settings_get_default()
4189 settings
.set_string_property('gtk-icon-theme-name', \
4190 'gpodder', __file__
)
4191 # Extend the search path for the optified icon theme (Maemo 5)
4192 icon_theme
= gtk
.icon_theme_get_default()
4193 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
4195 gtk
.window_set_default_icon_name('gpodder')
4196 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
4199 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
4200 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
4202 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
4203 except dbus
.exceptions
.DBusException
, dbe
:
4204 log('Warning: Cannot get "on the bus".', traceback
=True)
4205 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
4206 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
4207 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
4208 dlg
.set_title('gPodder')
4213 util
.make_directory(gpodder
.home
)
4214 gpodder
.load_plugins()
4216 config
= UIConfig(gpodder
.config_file
)
4218 # Load hook modules and install the hook manager globally
4219 # if modules have been found an instantiated by the manager
4220 user_hooks
= hooks
.HookManager()
4221 if user_hooks
.has_modules():
4222 gpodder
.user_hooks
= user_hooks
4224 if gpodder
.ui
.diablo
:
4225 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4226 # folder exists there (allow moving "gpodder" between SD cards or USB)
4227 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4228 if not os
.path
.exists(config
.download_dir
):
4229 log('Downloads might have been moved. Trying to locate them...')
4230 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4231 dir = os
.path
.join(basedir
, 'gpodder')
4232 if os
.path
.exists(dir):
4233 log('Downloads found in: %s', dir)
4234 config
.download_dir
= dir
4237 log('Downloads NOT FOUND in %s', dir)
4238 elif gpodder
.ui
.fremantle
:
4239 config
.on_quit_ask
= False
4241 if config
.enable_fingerscroll
:
4242 BuilderWidget
.use_fingerscroll
= True
4244 config
.mygpo_device_type
= util
.detect_device_type()
4246 gp
= gPodder(bus_name
, config
)
4249 if options
.subscribe
:
4250 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4253 # handle "subscribe to podcast" events from firefox
4254 if platform
.system() == 'Darwin':
4255 from gpodder
import gpodderosx
4256 gpodderosx
.register_handlers(gp
)
4257 # end mac OS X stuff